From c92fea5d46f8c7968498c6e817757f64043cc43d Mon Sep 17 00:00:00 2001 From: Eric Zhao Date: Mon, 23 Jul 2018 15:51:05 +0800 Subject: [PATCH] Welcome to the world, Sentinel --- .circleci/config.yml | 16 + .github/ISSUE_TEMPLATE.md | 28 + .github/PULL_REQUEST_TEMPLATE.md | 19 + .gitignore | 27 + .travis.yml | 10 + CODE_OF_CONDUCT.md | 73 + CONTRIBUTING.md | 80 + LICENSE | 201 + README.md | 131 + doc/image.gif | Bin 0 -> 28745 bytes doc/image/nolockcirclearray.gif | Bin 0 -> 19838 bytes doc/image/slots.gif | Bin 0 -> 157440 bytes pom.xml | 218 + sentinel-adapter/pom.xml | 68 + .../sentinel-dubbo-adapter/README.md | 53 + .../sentinel-dubbo-adapter/pom.xml | 44 + .../adapter/dubbo/AbstractDubboFilter.java | 44 + .../adapter/dubbo/DubboAppContextFilter.java | 43 + .../sentinel/adapter/dubbo/DubboUtils.java | 35 + .../dubbo/SentinelDubboConsumerFilter.java | 70 + .../dubbo/SentinelDubboProviderFilter.java | 75 + .../dubbo/com.alibaba.dubbo.rpc.Filter | 3 + .../sentinel/adapter/dubbo/DemoService.java | 23 + .../dubbo/provider/DemoServiceImpl.java | 27 + .../spring-dubbo-consumer-filter.xml | 18 + .../spring-dubbo-provider-filter.xml | 18 + .../sentinel-grpc-adapter/README.md | 38 + .../sentinel-grpc-adapter/pom.xml | 87 + .../grpc/SentinelGrpcClientInterceptor.java | 141 + .../grpc/SentinelGrpcServerInterceptor.java | 90 + .../adapter/grpc/FooServiceClient.java | 70 + .../sentinel/adapter/grpc/FooServiceImpl.java | 44 + .../SentinelGrpcClientInterceptorTest.java | 113 + .../SentinelGrpcServerInterceptorTest.java | 114 + .../src/test/proto/example.proto | 23 + .../sentinel-spring-boot-starter/README.md | 18 + .../sentinel-spring-boot-starter/pom.xml | 95 + .../com/alibaba/boot/sentinel/Constants.java | 31 + .../SentinelWebServletAutoConfiguration.java | 85 + .../endpoint/SentinelActuatorEndpoint.java | 68 + ...ndpointManagementContextConfiguration.java | 39 + .../sentinel/property/SentinelProperties.java | 93 + .../main/resources/META-INF/spring.factories | 5 + .../main/resources/META-INF/spring.provides | 1 + .../boot/sentinel/SimpleWebApplication.java | 53 + .../src/test/resources/web-servlet.properties | 3 + .../sentinel-web-servlet/README.md | 22 + sentinel-adapter/sentinel-web-servlet/pom.xml | 25 + .../adapter/servlet/CommonFilter.java | 88 + .../adapter/servlet/CommonTotalFilter.java | 88 + .../callback/DefaultUrlBlockHandler.java | 37 + .../servlet/callback/DefaultUrlCleaner.java | 27 + .../servlet/callback/UrlBlockHandler.java | 38 + .../adapter/servlet/callback/UrlCleaner.java | 31 + .../servlet/callback/WebCallbackManager.java | 50 + .../servlet/config/WebServletConfig.java | 42 + .../adapter/servlet/util/FilterUtil.java | 185 + sentinel-core/pom.xml | 27 + .../com/alibaba/csp/sentinel/Constants.java | 52 + .../java/com/alibaba/csp/sentinel/CtSph.java | 279 + .../java/com/alibaba/csp/sentinel/Entry.java | 141 + .../com/alibaba/csp/sentinel/EntryType.java | 47 + .../java/com/alibaba/csp/sentinel/Env.java | 38 + .../csp/sentinel/ErrorEntryFreeException.java | 28 + .../java/com/alibaba/csp/sentinel/Sph.java | 145 + .../java/com/alibaba/csp/sentinel/SphO.java | 225 + .../java/com/alibaba/csp/sentinel/SphU.java | 203 + .../java/com/alibaba/csp/sentinel/Tracer.java | 58 + .../concurrent/NamedThreadFactory.java | 50 + .../csp/sentinel/config/SentinelConfig.java | 149 + .../alibaba/csp/sentinel/context/Context.java | 164 + .../context/ContextNameDefineException.java | 26 + .../csp/sentinel/context/ContextUtil.java | 178 + .../csp/sentinel/context/NullContext.java | 33 + .../sentinel/eagleeye/BaseLoggerBuilder.java | 83 + .../csp/sentinel/eagleeye/EagleEye.java | 235 + .../sentinel/eagleeye/EagleEyeAppender.java | 45 + .../sentinel/eagleeye/EagleEyeCoreUtils.java | 237 + .../sentinel/eagleeye/EagleEyeLogDaemon.java | 140 + .../eagleeye/EagleEyeRollingFileAppender.java | 332 ++ .../csp/sentinel/eagleeye/FastDateFormat.java | 81 + .../csp/sentinel/eagleeye/StatEntry.java | 179 + .../csp/sentinel/eagleeye/StatEntryFunc.java | 206 + .../sentinel/eagleeye/StatLogController.java | 190 + .../csp/sentinel/eagleeye/StatLogger.java | 145 + .../sentinel/eagleeye/StatLoggerBuilder.java | 113 + .../sentinel/eagleeye/StatRollingData.java | 108 + .../csp/sentinel/eagleeye/SyncAppender.java | 79 + .../csp/sentinel/eagleeye/TokenBucket.java | 58 + .../csp/sentinel/init/InitExecutor.java | 101 + .../alibaba/csp/sentinel/init/InitFunc.java | 24 + .../alibaba/csp/sentinel/init/InitOrder.java | 41 + .../csp/sentinel/log/CommandCenterLog.java | 57 + .../csp/sentinel/log/CspFormatter.java | 54 + .../csp/sentinel/log/DateFileLogHandler.java | 124 + .../com/alibaba/csp/sentinel/log/LogBase.java | 83 + .../alibaba/csp/sentinel/log/LoggerUtils.java | 55 + .../alibaba/csp/sentinel/log/RecordLog.java | 53 + .../csp/sentinel/node/ClusterNode.java | 100 + .../csp/sentinel/node/DefaultNode.java | 151 + .../csp/sentinel/node/DefaultNodeBuilder.java | 35 + .../csp/sentinel/node/EntranceNode.java | 119 + .../csp/sentinel/node/IntervalProperty.java | 60 + .../com/alibaba/csp/sentinel/node/Node.java | 119 + .../csp/sentinel/node/NodeBuilder.java | 30 + .../sentinel/node/SampleCountProperty.java | 51 + .../csp/sentinel/node/StatisticNode.java | 189 + .../csp/sentinel/node/metric/MetricNode.java | 189 + .../sentinel/node/metric/MetricSearcher.java | 328 ++ .../node/metric/MetricTimerListener.java | 68 + .../sentinel/node/metric/MetricWriter.java | 349 ++ .../property/DynamicSentinelProperty.java | 77 + .../property/NoOpSentinelProperty.java | 32 + .../sentinel/property/PropertyListener.java | 38 + .../sentinel/property/SentinelProperty.java | 61 + .../property/SimplePropertyListener.java | 24 + .../AbstractLinkedProcessorSlot.java | 58 + .../slotchain/DefaultProcessorSlotChain.java | 83 + .../slotchain/MethodResourceWrapper.java | 58 + .../csp/sentinel/slotchain/ProcessorSlot.java | 74 + .../slotchain/ProcessorSlotChain.java | 38 + .../sentinel/slotchain/ResourceWrapper.java | 61 + .../slotchain/StringResourceWrapper.java | 50 + .../slots/DefaultSlotsChainBuilder.java | 52 + .../csp/sentinel/slots/SlotsChainBuilder.java | 32 + .../sentinel/slots/block/AbstractRule.java | 94 + .../sentinel/slots/block/BlockException.java | 101 + .../csp/sentinel/slots/block/Rule.java | 39 + .../sentinel/slots/block/RuleConstant.java | 41 + .../slots/block/SentinelRpcException.java | 38 + .../block/authority/AuthorityException.java | 43 + .../slots/block/authority/AuthorityRule.java | 104 + .../block/authority/AuthorityRuleManager.java | 156 + .../slots/block/authority/AuthoritySlot.java | 42 + .../slots/block/degrade/DegradeException.java | 41 + .../slots/block/degrade/DegradeRule.java | 238 + .../block/degrade/DegradeRuleManager.java | 163 + .../slots/block/degrade/DegradeSlot.java | 42 + .../slots/block/flow/ColdFactorProperty.java | 45 + .../sentinel/slots/block/flow/Controller.java | 27 + .../slots/block/flow/FlowException.java | 43 + .../sentinel/slots/block/flow/FlowRule.java | 297 + .../slots/block/flow/FlowRuleComparator.java | 42 + .../slots/block/flow/FlowRuleManager.java | 206 + .../sentinel/slots/block/flow/FlowSlot.java | 127 + .../flow/controller/DefaultController.java | 53 + .../block/flow/controller/PaceController.java | 77 + .../flow/controller/WarmUpController.java | 172 + .../clusterbuilder/ClusterBuilderSlot.java | 153 + .../slots/logger/EagleEyeLogUtil.java | 48 + .../csp/sentinel/slots/logger/LogSlot.java | 54 + .../slots/nodeselector/NodeSelectorSlot.java | 178 + .../slots/statistic/StatisticSlot.java | 127 + .../slots/statistic/base/LeapArray.java | 113 + .../slots/statistic/base/LongAdder.java | 207 + .../slots/statistic/base/Stripe64.java | 347 ++ .../sentinel/slots/statistic/base/Window.java | 100 + .../slots/statistic/base/WindowWrap.java | 68 + .../slots/statistic/metric/ArrayMetric.java | 228 + .../slots/statistic/metric/Metric.java | 118 + .../statistic/metric/WindowLeapArray.java | 92 + .../slots/system/SystemBlockException.java | 46 + .../csp/sentinel/slots/system/SystemRule.java | 170 + .../slots/system/SystemRuleManager.java | 303 + .../csp/sentinel/slots/system/SystemSlot.java | 44 + .../slots/system/SystemStatusListener.java | 68 + .../csp/sentinel/util/AppNameUtil.java | 92 + .../csp/sentinel/util/HostNameUtil.java | 80 + .../com/alibaba/csp/sentinel/util/IdUtil.java | 70 + .../alibaba/csp/sentinel/util/MethodUtil.java | 71 + .../alibaba/csp/sentinel/util/PidUtil.java | 31 + .../alibaba/csp/sentinel/util/StringUtil.java | 120 + .../alibaba/csp/sentinel/util/TimeUtil.java | 52 + .../csp/sentinel/ConfigPropertyHelper.java | 69 + .../com/alibaba/csp/sentinel/ContextTest.java | 35 + .../alibaba/csp/sentinel/RecordLogTest.java | 41 + .../com/alibaba/csp/sentinel/SphOTest.java | 171 + .../com/alibaba/csp/sentinel/SphUTest.java | 160 + .../sentinel/base/metric/ArrayMetricTest.java | 76 + .../base/metric/WindowLeapArrayTest.java | 184 + .../slots/block/degrade/DegradeTest.java | 98 + .../flow/FlowPartialIntegrationTest.java | 262 + .../slots/block/flow/FlowRuleTest.java | 177 + .../slots/block/flow/PaceControllerTest.java | 88 + .../block/flow/WarmUpControllerTest.java | 61 + .../slots/block/flow/WarmUpFlowTest.java | 54 + .../system/SystemGuardIntegrationTest.java | 23 + .../slots/block/system/SystemRuleTest.java | 66 + .../clusterbuilder/ClusterNodeBuilder.java | 66 + .../slots/nodeselector/NodeSelectorTest.java | 161 + sentinel-dashboard/README.md | 48 + .../Sentinel_Dashboard_Feature.md | 47 + sentinel-dashboard/pom.xml | 160 + .../csp/sentinel/dashboard/Application.java | 35 + .../sentinel/dashboard/config/WebConfig.java | 65 + .../datasource/entity/ApplicationEntity.java | 99 + .../datasource/entity/DegradeRuleEntity.java | 157 + .../datasource/entity/FlowRuleEntity.java | 218 + .../datasource/entity/MachineEntity.java | 124 + .../datasource/entity/MetricEntity.java | 223 + .../entity/MetricPositionEntity.java | 121 + .../datasource/entity/RuleEntity.java | 36 + .../datasource/entity/SystemRuleEntity.java | 146 + .../sentinel/dashboard/discovery/AppInfo.java | 56 + .../dashboard/discovery/AppManagement.java | 66 + .../dashboard/discovery/MachineDiscovery.java | 33 + .../dashboard/discovery/MachineInfo.java | 129 + .../discovery/SimpleMachineDiscovery.java | 55 + .../dashboard/domain/ResourceTreeNode.java | 242 + .../sentinel/dashboard/inmem/HttpHelper.java | 327 ++ .../inmem/InMemDegradeRuleStore.java | 35 + .../dashboard/inmem/InMemFlowRuleStore.java | 36 + .../dashboard/inmem/InMemMetricStore.java | 129 + .../inmem/InMemRepositoryAdapter.java | 96 + .../dashboard/inmem/InMemSystemRuleStore.java | 35 + .../dashboard/inmem/RuleRepository.java | 75 + .../dashboard/metric/MetricFetcher.java | 336 ++ .../dashboard/view/AppController.java | 81 + .../dashboard/view/DegradeController.java | 200 + .../dashboard/view/DemoController.java | 135 + .../dashboard/view/FlowController.java | 249 + .../sentinel/dashboard/view/HealthCheck.java | 33 + .../view/MachineRegistryController.java | 71 + .../dashboard/view/MetricController.java | 173 + .../dashboard/view/ResourceController.java | 88 + .../csp/sentinel/dashboard/view/Result.java | 76 + .../dashboard/view/SystemController.java | 214 + .../dashboard/view/vo/MachineInfoVo.java | 104 + .../sentinel/dashboard/view/vo/MetricVo.java | 208 + .../dashboard/view/vo/ResourceVo.java | 241 + .../src/main/resources/application.properties | 11 + .../src/main/webapp/resources/.gitignore | 2 + .../src/main/webapp/resources/.jshintrc | 67 + .../src/main/webapp/resources/README.md | 32 + .../src/main/webapp/resources/README_zh.md | 32 + .../main/webapp/resources/app/scripts/app.js | 163 + .../app/scripts/controllers/degrade.js | 187 + .../resources/app/scripts/controllers/flow.js | 200 + .../resources/app/scripts/controllers/home.js | 11 + .../app/scripts/controllers/identity.js | 286 + .../app/scripts/controllers/machine.js | 45 + .../resources/app/scripts/controllers/main.js | 10 + .../app/scripts/controllers/metric.js | 251 + .../app/scripts/controllers/system.js | 230 + .../app/scripts/directives/header/header.html | 9 + .../app/scripts/directives/header/header.js | 16 + .../sidebar-search/sidebar-search.html | 10 + .../sidebar/sidebar-search/sidebar-search.js | 20 + .../scripts/directives/sidebar/sidebar.html | 72 + .../app/scripts/directives/sidebar/sidebar.js | 62 + .../resources/app/scripts/filters/filters.js | 17 + .../resources/app/scripts/libs/treeTable.js | 292 + .../app/scripts/services/appservice.js | 12 + .../app/scripts/services/degradeservice.js | 63 + .../app/scripts/services/flowservice.js | 73 + .../app/scripts/services/identityservice.js | 30 + .../app/scripts/services/machineservice.js | 10 + .../app/scripts/services/metricservice.js | 36 + .../app/scripts/services/systemservice.js | 72 + .../main/webapp/resources/app/styles/main.css | 1476 +++++ .../main/webapp/resources/app/styles/page.css | 399 ++ .../webapp/resources/app/styles/timeline.css | 180 + .../resources/app/views/dashboard/home.html | 13 + .../resources/app/views/dashboard/main.html | 10 + .../webapp/resources/app/views/degrade.html | 102 + .../app/views/dialog/confirm-dialog.html | 20 + .../app/views/dialog/degrade-rule-dialog.html | 59 + .../app/views/dialog/flow-rule-dialog.html | 106 + .../app/views/dialog/system-rule-dialog.html | 57 + .../main/webapp/resources/app/views/flow.html | 111 + .../webapp/resources/app/views/identity.html | 106 + .../webapp/resources/app/views/machine.html | 94 + .../webapp/resources/app/views/metric.html | 117 + .../resources/app/views/pagination.tpl.html | 18 + .../webapp/resources/app/views/system.html | 90 + .../main/webapp/resources/dist/css/app.css | 5 + .../src/main/webapp/resources/dist/js/app.js | 1 + .../webapp/resources/dist/js/app.vendor.js | 1 + .../src/main/webapp/resources/gulpfile.js | 126 + .../src/main/webapp/resources/index.htm | 28 + .../src/main/webapp/resources/index_dev.htm | 28 + .../main/webapp/resources/license-stat.csv | 26 + .../main/webapp/resources/package-lock.json | 5015 +++++++++++++++++ .../src/main/webapp/resources/package.json | 54 + sentinel-demo/README.md | 9 + sentinel-demo/pom.xml | 33 + sentinel-demo/sentinel-demo-basic/pom.xml | 17 + .../degrade/ExceptionRatioDegradeDemo.java | 170 + .../sentinel/demo/degrade/RtDegradeDemo.java | 184 + .../csp/sentinel/demo/flow/FlowQpsDemo.java | 161 + .../sentinel/demo/flow/FlowThreadDemo.java | 155 + .../csp/sentinel/demo/flow/PaceFlowDemo.java | 204 + .../sentinel/demo/flow/WarmUpFlowDemo.java | 224 + .../sentinel/demo/system/SystemGuardDemo.java | 145 + sentinel-demo/sentinel-demo-dubbo/README.md | 78 + sentinel-demo/sentinel-demo-dubbo/pom.xml | 51 + .../csp/sentinel/demo/dubbo/FooService.java | 25 + .../dubbo/consumer/ConsumerConfiguration.java | 58 + .../dubbo/consumer/FooServiceConsumer.java | 36 + .../dubbo/demo1/FooConsumerBootstrap.java | 58 + .../dubbo/demo1/FooProviderBootstrap.java | 61 + .../demo/dubbo/demo1/FooServiceImpl.java | 38 + .../dubbo/demo1/ProviderConfiguration.java | 54 + .../dubbo/demo2/FooConsumerBootstrap.java | 85 + .../dubbo/demo2/FooProviderBootstrap.java | 43 + .../demo/dubbo/demo2/FooServiceImpl.java | 38 + .../dubbo/demo2/ProviderConfiguration.java | 54 + .../sentinel-demo-dynamic-file-rule/pom.xml | 24 + .../demo/file/rule/FileDataSourceDemo.java | 96 + .../demo/file/rule/FlowQpsRunner.java | 132 + .../parser/JsonDegradeRuleListParser.java | 35 + .../rule/parser/JsonFlowRuleListParser.java | 35 + .../rule/parser/JsonSystemRuleListParser.java | 35 + .../src/main/resources/DegradeRule.json | 16 + .../src/main/resources/FlowRule.json | 18 + .../src/main/resources/SystemRule.json | 8 + .../sentinel-demo-rocketmq/README.md | 15 + sentinel-demo/sentinel-demo-rocketmq/pom.xml | 36 + .../csp/sentinel/demo/rocketmq/Constants.java | 24 + .../demo/rocketmq/PullConsumerDemo.java | 151 + .../sentinel/demo/rocketmq/SyncProducer.java | 43 + sentinel-extension/README.md | 9 + sentinel-extension/pom.xml | 18 + .../sentinel-datasource-extension/pom.xml | 30 + .../datasource/AbstractDataSource.java | 56 + .../datasource/AutoRefreshDataSource.java | 74 + .../csp/sentinel/datasource/ConfigParser.java | 31 + .../csp/sentinel/datasource/DataSource.java | 66 + .../sentinel/datasource/EmptyDataSource.java | 60 + .../datasource/FileRefreshableDataSource.java | 134 + sentinel-transport/pom.xml | 42 + .../sentinel-transport-common/pom.xml | 24 + .../csp/sentinel/command/CommandHandler.java | 32 + .../command/CommandHandlerProvider.java | 70 + .../csp/sentinel/command/CommandRequest.java | 75 + .../csp/sentinel/command/CommandResponse.java | 83 + .../command/annotation/CommandMapping.java | 33 + .../handler/BasicInfoCommandHandler.java | 36 + .../FetchActiveRuleCommandHandler.java | 50 + .../FetchClusterNodeByIdCommandHandler.java | 47 + .../FetchClusterNodeHumanCommandHandler.java | 92 + .../handler/FetchJsonTreeCommandHandler.java | 56 + .../handler/FetchOriginCommandHandler.java | 112 + .../FetchSimpleClusterNodeCommandHandler.java | 61 + .../FetchSystemStatusCommandHandler.java | 47 + .../handler/FetchTreeCommandHandler.java | 92 + .../handler/ModifyRulesCommandHandler.java | 137 + .../handler/OnOffGetCommandHandler.java | 34 + .../handler/OnOffSetCommandHandler.java | 46 + .../handler/SendMetricCommandHandler.java | 96 + .../handler/VersionCommandHandler.java | 34 + .../csp/sentinel/command/vo/NodeVo.java | 238 + .../csp/sentinel/transport/CommandCenter.java | 45 + .../sentinel/transport/HeartbeatSender.java | 43 + .../transport/client/CommandClient.java | 38 + .../transport/config/TransportConfig.java | 65 + .../transport/init/CommandCenterInitFunc.java | 46 + .../init/HeartbeatSenderInitFunc.java | 74 + .../transport/util/HttpCommandUtils.java | 37 + ...libaba.csp.sentinel.command.CommandHandler | 14 + .../com.alibaba.csp.sentinel.init.InitFunc | 2 + .../sentinel-transport-netty-http/pom.xml | 41 + .../command/NettyHttpCommandCenter.java | 67 + .../command/codec/CodecRegistry.java | 56 + .../transport/command/codec/Decoder.java | 54 + .../command/codec/DefaultCodecs.java | 30 + .../transport/command/codec/Encoder.java | 54 + .../command/codec/StringDecoder.java | 46 + .../command/codec/StringEncoder.java | 45 + .../transport/command/netty/HttpServer.java | 95 + .../command/netty/HttpServerHandler.java | 210 + .../command/netty/HttpServerInitializer.java | 40 + .../heartbeat/HttpHeartbeatSender.java | 102 + ...ibaba.csp.sentinel.transport.CommandCenter | 1 + ...aba.csp.sentinel.transport.HeartbeatSender | 1 + .../sentinel-transport-simple-http/pom.xml | 21 + .../command/SimpleHttpCommandCenter.java | 242 + .../transport/command/http/HttpEventTask.java | 229 + .../transport/heartbeat/HeartbeatMessage.java | 53 + .../heartbeat/SimpleHttpHeartbeatSender.java | 118 + .../heartbeat/client/SimpleHttpClient.java | 190 + .../heartbeat/client/SimpleHttpRequest.java | 99 + .../heartbeat/client/SimpleHttpResponse.java | 124 + .../client/SimpleHttpResponseParser.java | 154 + ...ibaba.csp.sentinel.transport.CommandCenter | 1 + ...aba.csp.sentinel.transport.HeartbeatSender | 1 + 386 files changed, 40030 insertions(+) create mode 100755 .circleci/config.yml create mode 100755 .github/ISSUE_TEMPLATE.md create mode 100755 .github/PULL_REQUEST_TEMPLATE.md create mode 100755 .gitignore create mode 100755 .travis.yml create mode 100755 CODE_OF_CONDUCT.md create mode 100755 CONTRIBUTING.md create mode 100755 LICENSE create mode 100755 README.md create mode 100755 doc/image.gif create mode 100755 doc/image/nolockcirclearray.gif create mode 100755 doc/image/slots.gif create mode 100755 pom.xml create mode 100755 sentinel-adapter/pom.xml create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/README.md create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/pom.xml create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java create mode 100644 sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java create mode 100644 sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DemoService.java create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoServiceImpl.java create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-consumer-filter.xml create mode 100755 sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-provider-filter.xml create mode 100755 sentinel-adapter/sentinel-grpc-adapter/README.md create mode 100755 sentinel-adapter/sentinel-grpc-adapter/pom.xml create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java create mode 100755 sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/README.md create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/pom.xml create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/Constants.java create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/config/SentinelWebServletAutoConfiguration.java create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelActuatorEndpoint.java create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelEndpointManagementContextConfiguration.java create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/property/SentinelProperties.java create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.provides create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/test/java/com/alibaba/boot/sentinel/SimpleWebApplication.java create mode 100755 sentinel-adapter/sentinel-spring-boot-starter/src/test/resources/web-servlet.properties create mode 100755 sentinel-adapter/sentinel-web-servlet/README.md create mode 100755 sentinel-adapter/sentinel-web-servlet/pom.xml create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java create mode 100755 sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java create mode 100755 sentinel-core/pom.xml create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/Entry.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/EntryType.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/Env.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/ErrorEntryFreeException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphO.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/Tracer.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/concurrent/NamedThreadFactory.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/Context.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextNameDefineException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/NullContext.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/BaseLoggerBuilder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEye.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeAppender.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtils.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeLogDaemon.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeRollingFileAppender.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/FastDateFormat.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntry.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntryFunc.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogController.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogger.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLoggerBuilder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatRollingData.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/SyncAppender.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/TokenBucket.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitFunc.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitOrder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CommandCenterLog.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/DateFileLogHandler.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LoggerUtils.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/RecordLog.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNode.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNodeBuilder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/EntranceNode.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/IntervalProperty.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/NodeBuilder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/SampleCountProperty.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricSearcher.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricWriter.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/DynamicSentinelProperty.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/NoOpSentinelProperty.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/PropertyListener.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SentinelProperty.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SimplePropertyListener.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/AbstractLinkedProcessorSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotChain.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotsChainBuilder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/SlotsChainBuilder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/AbstractRule.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/BlockException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/Rule.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/SentinelRpcException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRule.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRuleManager.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthoritySlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/ColdFactorProperty.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/Controller.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRule.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleComparator.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleManager.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/DefaultController.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/PaceController.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/WarmUpController.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/LogSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Stripe64.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Window.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/WindowLeapArray.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemBlockException.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRule.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemSlot.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/HostNameUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/IdUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/MethodUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/PidUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/StringUtil.java create mode 100755 sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/TimeUtil.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConfigPropertyHelper.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/ArrayMetricTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/WindowLeapArrayTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowPartialIntegrationTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/PaceControllerTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpControllerTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpFlowTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemGuardIntegrationTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemRuleTest.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterNodeBuilder.java create mode 100755 sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorTest.java create mode 100755 sentinel-dashboard/README.md create mode 100755 sentinel-dashboard/Sentinel_Dashboard_Feature.md create mode 100755 sentinel-dashboard/pom.xml create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/Application.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/config/WebConfig.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/DegradeRuleEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/FlowRuleEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MachineEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/RuleEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/SystemRuleEntity.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppInfo.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppManagement.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineDiscovery.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineInfo.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/domain/ResourceTreeNode.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/HttpHelper.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemDegradeRuleStore.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemFlowRuleStore.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemMetricStore.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemRepositoryAdapter.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemSystemRuleStore.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/RuleRepository.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/metric/MetricFetcher.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/AppController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DegradeController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DemoController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/FlowController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/HealthCheck.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MachineRegistryController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MetricController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/ResourceController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/Result.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/SystemController.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MachineInfoVo.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MetricVo.java create mode 100755 sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/ResourceVo.java create mode 100755 sentinel-dashboard/src/main/resources/application.properties create mode 100755 sentinel-dashboard/src/main/webapp/resources/.gitignore create mode 100755 sentinel-dashboard/src/main/webapp/resources/.jshintrc create mode 100755 sentinel-dashboard/src/main/webapp/resources/README.md create mode 100644 sentinel-dashboard/src/main/webapp/resources/README_zh.md create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/degrade.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/home.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/machine.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/main.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/metric.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/filters/filters.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/libs/treeTable.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/appservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/degradeservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/flowservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/identityservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/metricservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/scripts/services/systemservice.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/styles/main.css create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/styles/page.css create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/styles/timeline.css create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/home.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/main.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/degrade.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/dialog/confirm-dialog.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/flow.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/identity.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/machine.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/metric.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/pagination.tpl.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/app/views/system.html create mode 100755 sentinel-dashboard/src/main/webapp/resources/dist/css/app.css create mode 100755 sentinel-dashboard/src/main/webapp/resources/dist/js/app.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/dist/js/app.vendor.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/gulpfile.js create mode 100755 sentinel-dashboard/src/main/webapp/resources/index.htm create mode 100755 sentinel-dashboard/src/main/webapp/resources/index_dev.htm create mode 100755 sentinel-dashboard/src/main/webapp/resources/license-stat.csv create mode 100644 sentinel-dashboard/src/main/webapp/resources/package-lock.json create mode 100755 sentinel-dashboard/src/main/webapp/resources/package.json create mode 100755 sentinel-demo/README.md create mode 100755 sentinel-demo/pom.xml create mode 100755 sentinel-demo/sentinel-demo-basic/pom.xml create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/RtDegradeDemo.java create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowQpsDemo.java create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowThreadDemo.java create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/PaceFlowDemo.java create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/WarmUpFlowDemo.java create mode 100755 sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/system/SystemGuardDemo.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/README.md create mode 100644 sentinel-demo/sentinel-demo-dubbo/pom.xml create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/FooService.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/ConsumerConfiguration.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/FooServiceConsumer.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooConsumerBootstrap.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooProviderBootstrap.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooServiceImpl.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/ProviderConfiguration.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooConsumerBootstrap.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooProviderBootstrap.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooServiceImpl.java create mode 100644 sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/ProviderConfiguration.java create mode 100755 sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceDemo.java create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FlowQpsRunner.java create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonDegradeRuleListParser.java create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonFlowRuleListParser.java create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonSystemRuleListParser.java create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/DegradeRule.json create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/FlowRule.json create mode 100644 sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/SystemRule.json create mode 100644 sentinel-demo/sentinel-demo-rocketmq/README.md create mode 100755 sentinel-demo/sentinel-demo-rocketmq/pom.xml create mode 100755 sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/Constants.java create mode 100755 sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/PullConsumerDemo.java create mode 100755 sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/SyncProducer.java create mode 100755 sentinel-extension/README.md create mode 100755 sentinel-extension/pom.xml create mode 100755 sentinel-extension/sentinel-datasource-extension/pom.xml create mode 100755 sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AbstractDataSource.java create mode 100755 sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AutoRefreshDataSource.java create mode 100755 sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/ConfigParser.java create mode 100755 sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/DataSource.java create mode 100755 sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/EmptyDataSource.java create mode 100755 sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileRefreshableDataSource.java create mode 100755 sentinel-transport/pom.xml create mode 100755 sentinel-transport/sentinel-transport-common/pom.xml create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandRequest.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandResponse.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/annotation/CommandMapping.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/BasicInfoCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchActiveRuleCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeByIdCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeHumanCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchJsonTreeCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchOriginCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSimpleClusterNodeCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSystemStatusCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTreeCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffGetCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffSetCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/SendMetricCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/VersionCommandHandler.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/vo/NodeVo.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/CommandCenter.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/HeartbeatSender.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/client/CommandClient.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/CommandCenterInitFunc.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/HeartbeatSenderInitFunc.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/util/HttpCommandUtils.java create mode 100755 sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler create mode 100755 sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc create mode 100755 sentinel-transport/sentinel-transport-netty-http/pom.xml create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/NettyHttpCommandCenter.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/CodecRegistry.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Decoder.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/DefaultCodecs.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Encoder.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringDecoder.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringEncoder.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServer.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializer.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter create mode 100755 sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender create mode 100755 sentinel-transport/sentinel-transport-simple-http/pom.xml create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/SimpleHttpCommandCenter.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HeartbeatMessage.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpClient.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpRequest.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponse.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponseParser.java create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter create mode 100755 sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100755 index 00000000..7da6d4b0 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,16 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/openjdk:8-jdk + + working_directory: ~/sentinel + + environment: + MAVEN_OPTS: -Xmx3200m + + steps: + - checkout + + # Run tests + - run: mvn integration-test \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100755 index 00000000..f04e94d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ + + +## Issue Description + +Type: *bug report* or *feature request* + +### Describe what happened (or what feature you want) + + +### Describe what you expected to happen + + +### How to reproduce it (as minimally and precisely as possible) + +1. +2. +3. + +### Tell us your environment + + +### Anything else we need to know? + + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100755 index 00000000..6c009260 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + + +### Describe what this PR does / why we need it + + +### Does this pull request fix one issue? + + + +### Describe how you did it + + +### Describe how to verify it + + +### Special notes for reviews diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..84c94f69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# IntelliJ project files +.idea/ +*.iml +out +gen + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +!/.mvn/wrapper/maven-wrapper.jar + +# Eclipse +.classpath +.settings/ +.project +bin/ + +# System related +*.DS_Store +Thumbs.db diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 00000000..32dc91db --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: java + +sudo: required +dist: trusty + +jdk: + - oraclejdk8 + +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 00000000..ddd80669 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at sentinel@linux.alibaba.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 00000000..4618d955 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing to Sentinel + +Welcome to Sentinel! This document is a guideline about how to contribute to Sentinel. +If you find something incorrect or missing, please leave comments / suggestions. + +# Before you get started + +## Code of Conduct + +Please make sure to read and observe our [Code of Conduct](./CODE_OF_CONDUCT.md). + +## Setting up your development environment + +You should have JDK 1.8 or later installed in your system. + +# Contributing + +We are always very happy to have contributions, whether for typo fix, bug fix or big new features. +Please do not ever hesitate to ask a question or send a pull request. + +We strongly value documentation and integration with other projects. +We are very glad to accept improvements for these aspects. + +## GitHub workflow + +We use the `master` branch as the development branch, which indicates that this is a unstable branch. + +Here are the workflow for contributors: + +1. Fork to your own +2. Clone fork to local repository +3. Create a new branch and work on it +4. Keep your branch in sync +5. Commit your changes (make sure your commit message concise) +6. Push your commits to your forked repository +7. Create a pull request + +Please follow [the pull request template](./.github/PULL_REQUEST_TEMPLATE.md). +Please make sure the PR has a corresponding issue. + +After creating a PR, one or more reviewers will be assigned to the pull request. +The reviewers will review the code. + +Before merging a PR, squash any fix review feedback, typo, merged, and rebased sorts of commits. +The final commit message should be clear and concise. + +## Open an issue / PR + +We use [GitHub Issues](https://github.com/alibaba/Sentinel/issues) and [Pull Requests](https://github.com/alibaba/Sentinel/pulls) for trackers. + +If you find a typo in document, find a bug in code, or want new features, or want to give suggestions, +you can [open an issue on GitHub](https://github.com/alibaba/Sentinel/issues/new) to report it. +Please follow the guideline message in the issue template. + +If you want to contribute, please follow the [contribution workflow](#github-workflow) and create a new pull request. +If your PR contains large changes, e.g. component refactor or new components, please write detailed documents +about its design and usage. + +Note that a single PR should not be too large. If heavy changes are required, it's better to separate the changes +to a few individual PRs. + +## Code review + +All code should be well reviewed by one or more committers. Some principles: + +- Readability: Important code should be well-documented. Comply with our code style. +- Elegance: New functions, classes or components should be well designed. +- Testability: Important code should be well-tested (high unit test coverage). + +# Community + +## Contact us + +### Mailing list + +If you have any questions or advice, please contact sentinel@linux.alibaba.com. + +### Gitter + +Our Gitter room: [https://gitter.im/alibaba/Sentinel](https://gitter.im/alibaba/Sentinel). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/README.md b/README.md new file mode 100755 index 00000000..0d52c69e --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# Sentinel: Sentinel of Your Application + +[![Travis Build Status](https://travis-ci.org/alibaba/Sentinel.svg?branch=master)](https://travis-ci.org/alibaba/Sentinel) +[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) +[![Gitter](https://badges.gitter.im/alibaba/Sentinel.svg)](https://gitter.im/alibaba/Sentinel) + +## What Does It Do? + +As distributed systems become increasingly popular, the stability between services is becoming more important than ever before. Sentinel takes "flow" as breakthrough point, and works on multiple fields including **flow control**, **concurrency**, **circuit breaking** and **load protection**, to protect service stability. + +Sentinel has the following features: + +* **Rich applicable scenarios**: +Sentinel has been wildly used in Alibaba, and has covered almost all the core-scenarios in Double-11 Shopping Festivals in the past 10 years, such as “Second Kill” which needs to limit burst flow traffic to meet the system capacity, message peak clipping and valley fills, degrading unreliable downstream applications, etc. + +* **Integrated monitor module**: +Sentinel also provides real-time monitoring function. You can see the runtime information of a single machine in real-time, and the summary runtime info of a cluster with less than 500 nodes. + +* **Easy extension point**: +Sentinel provides easy-to-use extension points that allow you to quickly customize your logic, for example, custom rule management, adapting data sources, and so on. + +## Documentation + +See the [中文文档](https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D) for Chinese readme. + +See the [Wiki](https://github.com/alibaba/Sentinel/wiki) for full documentation, examples, operational details and other information. + +See the [Javadoc](https://github.com/alibaba/Sentinel/tree/master/doc) for the API. + +## Quick Start + +Below is a simple demo that guides new users to use Sentinel in just 3 steps. It also shows how to monitor this demo using the dashboard. + +### 1.Download Library + +**Note:** Sentinel requires Java 6 or later. + +If your application is build in maven, just add the following code in pom.xml. + +```xml + + com.alibaba.csp + sentinel-core + x.y.z + +``` + +If not, you can download JAR in [Maven Center Repository](https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-core). + + +### 2.Define Resource + +Wrap code snippet via Sentinel API: `SphU.entry("RESOURCENAME")` and `entry.exit()`. In below example, it is `System.out.println("hello world");`: + +```java +Entry entry = null; + +try { + entry = SphU.entry("HelloWorld"); + + // BIZ logic being protected + System.out.println("hello world"); +} catch (BlockException e) { + // handle block logic +} finally { + // make sure that the exit() logic is called + if (entry != null) { + entry.exit(); + } +} +``` + +So far the code modification is done. + +### 3.Define Rules + +If we want to limit the access times of the resource, we can define rules. The following code defines a rule that limits access to the reource to 20 times per second at the maximum. + +```java +List rules = new ArrayList(); +FlowRule rule = new FlowRule(); +rule.setResource("hello world"); +// set limit qps to 20 +rule.setCount(20); +rules.add(rule); +FlowRuleManager.loadRules(rules); +``` + +### 4. Check the Result + +After running the demo for a while, you can see the following records in `~/logs/csp/${appName}-metrics.log.xxx`. + +``` +|--timestamp-|------date time----|--resource-|p |block|s |e|rt +1529998904000|2018-06-26 15:41:44|hello world|20|0 |20|0|0 +1529998905000|2018-06-26 15:41:45|hello world|20|5579 |20|0|728 +1529998906000|2018-06-26 15:41:46|hello world|20|15698|20|0|0 +1529998907000|2018-06-26 15:41:47|hello world|20|19262|20|0|0 +1529998908000|2018-06-26 15:41:48|hello world|20|19502|20|0|0 +1529998909000|2018-06-26 15:41:49|hello world|20|18386|20|0|0 + +p stands for incoming request, block for intercepted by rules, success for success handled, e for exception, rt for average response time (ms) +``` +This shows that the demo can print "hello world" 20 times per second. + +More examples and information can be found in the [How To Use](https://github.com/alibaba/Sentinel/wiki/How-to-Use) section. + +The working principles of Sentinel can be found in [How it works](https://github.com/alibaba/Sentinel/wiki/How-it-works) section. + +Samples can be found in the [sentinel-demo](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo) module. + +### 5.Start Dashboard + +Sentinel also provides a simple dashboard application, on which you can monitor the clients and configure the rules in real time. + +For details please refer to [Dashboard](https://github.com/alibaba/Sentinel/wiki/Dashboard). + +## Trouble Shooting and Logs + +Sentinel will generate logs for troubleshooting. All the information can be found in [logs](https://github.com/alibaba/Sentinel/wiki/Logs). + +## Bugs and Feedback + +For bug report, questions and discussions please submit [GitHub Issues](https://github.com/alibaba/sentinel/issues). + +Contact us: sentinel@linux.alibaba.com + +## Contributing + +Contributions are always welcomed! Please see [CONTRIBUTING](./CONTRIBUTING.md) for detailed guidelines. + diff --git a/doc/image.gif b/doc/image.gif new file mode 100755 index 0000000000000000000000000000000000000000..51be223d2398a634c2f079f892363b216a0016e4 GIT binary patch literal 28745 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 Ud~wDbcl>e4BbR(~r~?83JDLbbf&c&j literal 0 HcmV?d00001 diff --git a/doc/image/nolockcirclearray.gif b/doc/image/nolockcirclearray.gif new file mode 100755 index 0000000000000000000000000000000000000000..1a65fc9841886cf0785644aef09ac4dd9184dac5 GIT binary patch literal 19838 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 X+N^Eg`90m$9oqk`-*UYV0RaFzlPX9n literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100755 index 00000000..1351d8c0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,218 @@ + + + 4.0.0 + + com.alibaba.csp + sentinel-parent + 0.1.0 + pom + + ${project.artifactId} + The parent project of Sentinel + https://github.com/alibaba/Sentinel + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + https://github.com/alibaba/Sentinel + scm:git:https://github.com/alibaba/Sentinel.git + scm:git:https://github.com/alibaba/Sentinel.git + + + + The Sentinel Project Contributors + sentinel-dev@linux.alibaba.com + https://github.com/alibaba/Sentinel + + + + Alibaba Group + https://github.com/alibaba + + + github + https://github.com/alibaba/Sentinel/issues + + + + + 1.2.47 + + + 4.12 + 2.18.0 + + + UTF-8 + 1.6 + 1.6 + UTF-8 + 3.7.0 + 3.0.1 + 3.0.1 + 1.6 + 0.8.1 + + + + sentinel-core + sentinel-extension + sentinel-transport + sentinel-adapter + sentinel-dashboard + + sentinel-demo + + + + + + com.alibaba.csp + sentinel-core + ${project.version} + + + com.alibaba.csp + sentinel-extension + ${project.version} + + + com.alibaba.csp + sentinel-datasource-extension + ${project.version} + + + com.alibaba.csp + sentinel-adapter + ${project.version} + + + + com.alibaba + fastjson + ${fastjson.version} + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + ${java.source.version} + ${java.target.version} + ${java.encoding} + + + + org.jacoco + jacoco-maven-plugin + ${maven.jacoco.version} + + + + prepare-agent + + + + report + test + + report + + + + + + + + + + oss + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven.source.version} + + + package + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven.javadoc.version} + + + package + + jar + + + en_US + UTF-8 + UTF-8 + none + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven.gpg.version} + + + verify + + sign + + + + + + + + + oss + https://oss.sonatype.org/content/repositories/snapshots/ + + + oss + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + \ No newline at end of file diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml new file mode 100755 index 00000000..bd1acec8 --- /dev/null +++ b/sentinel-adapter/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + + com.alibaba.csp + sentinel-parent + 0.1.0 + + sentinel-adapter + pom + sentinel-adapter + The adapters of Sentinel + + + sentinel-web-servlet + sentinel-dubbo-adapter + sentinel-grpc-adapter + sentinel-spring-boot-starter + + + + 3.1.0 + 2.5.8 + 1.13.1 + + + + + + com.alibaba.csp + sentinel-core + ${project.version} + + + com.alibaba.csp + sentinel-extension + ${project.version} + + + javax.servlet + javax.servlet-api + ${servlet.api.version} + provided + + + + com.alibaba + dubbo + ${dubbo.version} + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + diff --git a/sentinel-adapter/sentinel-dubbo-adapter/README.md b/sentinel-adapter/sentinel-dubbo-adapter/README.md new file mode 100755 index 00000000..a897de5b --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/README.md @@ -0,0 +1,53 @@ +# Sentinel Dubbo Adapter + +Sentinel Dubbo Adapter provides service consumer filter and provider filter +for [Dubbo](http://dubbo.io/) services. + +To use Sentinel Dubbo Adapter, you can simply add the following dependency to your `pom.xml`: + +```xml + + com.alibaba.csp + sentinel-dubbo-adapter + x.y.z + +``` + +The Sentinel filters are **enabled by default**. Once you add the dependency, +the Dubbo services and methods will become protected resources in Sentinel, +which can leverage Sentinel's flow control and guard ability when rules are configured. +Demos can be found in [sentinel-demo-dubbo](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-dubbo). + +If you don't want the filters enabled, you can manually disable them. For example: + +```xml + + + +``` + +For more details of Dubbo filter, see [here](https://dubbo.incubator.apache.org/#/docs/dev/impls/filter.md?lang=en-us). + +## Dubbo resources + +The resource for Dubbo services has two granularities: service interface and service method. + +- Service interface:resourceName format is `interfaceName`,e.g. `com.alibaba.csp.sentinel.demo.dubbo.FooService` +- Service method:resourceName format is `interfaceName:methodSignature`,e.g. `com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)` + +## Flow control based on caller + +In many circumstances, it's also significant to control traffic flow based on the **caller**. +For example, assuming that there are two services A and B, both of them initiate remote call requests to the service provider. +If we want to limit the calls from service B only, we can set the `limitApp` of flow rule as the identifier of service B (e.g. service name). + +Sentinel Dubbo Adapter will automatically resolve the Dubbo consumer's *application name* as the caller's name (`origin`), +and will bring the caller's name when doing resource protection. +If `limitApp` of flow rules is not configured (`default`), flow control will take effects on all callers. +If `limitApp` of a flow rule is configured with a caller, then the corresponding flow rule will only take effect on the specific caller. + +> Note: Dubbo consumer does not provide its Dubbo application name when doing RPC, +so developers should manually put the application name into *attachment* at consumer side, +then extract it at provider side. Sentinel Dubbo Adapter has implemented a filter (`DubboAppContextFilter`) +where consumer can carry application name information to provider automatically. +If the consumer does not use Sentinel Dubbo Adapter but requires flow control based on caller, developers can manually put the application name into attachment with the key `dubboApplication`. \ No newline at end of file diff --git a/sentinel-adapter/sentinel-dubbo-adapter/pom.xml b/sentinel-adapter/sentinel-dubbo-adapter/pom.xml new file mode 100755 index 00000000..a9913e66 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/pom.xml @@ -0,0 +1,44 @@ + + + + + + com.alibaba.csp + sentinel-adapter + 0.1.0 + + 4.0.0 + sentinel-dubbo-adapter + jar + + + + com.alibaba.csp + sentinel-core + + + com.alibaba + dubbo + provided + + + + junit + junit + test + + + + org.mockito + mockito-core + test + + + com.alibaba + fastjson + 1.2.47 + test + + + diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java new file mode 100755 index 00000000..2c5b0779 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/AbstractDubboFilter.java @@ -0,0 +1,44 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo; + +import com.alibaba.dubbo.rpc.Filter; +import com.alibaba.dubbo.rpc.Invocation; +import com.alibaba.dubbo.rpc.Invoker; + +/** + * @author leyou + */ +abstract class AbstractDubboFilter implements Filter { + + protected String getResourceName(Invoker invoker, Invocation invocation) { + StringBuilder buf = new StringBuilder(64); + buf.append(invoker.getInterface().getName()) + .append(":") + .append(invocation.getMethodName()) + .append("("); + boolean isFirst = true; + for (Class clazz : invocation.getParameterTypes()) { + if (!isFirst) { + buf.append(","); + } + buf.append(clazz.getName()); + isFirst = false; + } + buf.append(")"); + return buf.toString(); + } +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java new file mode 100644 index 00000000..ceae979d --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboAppContextFilter.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo; + +import com.alibaba.dubbo.common.Constants; +import com.alibaba.dubbo.common.extension.Activate; +import com.alibaba.dubbo.rpc.Filter; +import com.alibaba.dubbo.rpc.Invocation; +import com.alibaba.dubbo.rpc.Invoker; +import com.alibaba.dubbo.rpc.Result; +import com.alibaba.dubbo.rpc.RpcContext; +import com.alibaba.dubbo.rpc.RpcException; + +/** + * Puts current consumer's application name in the attachment of each invocation. + * + * @author Eric Zhao + */ +@Activate(group = "consumer") +public class DubboAppContextFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + String application = invoker.getUrl().getParameter(Constants.APPLICATION_KEY); + if (application != null) { + RpcContext.getContext().setAttachment(DubboUtils.DUBBO_APPLICATION_KEY, application); + } + return invoker.invoke(invocation); + } +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java new file mode 100644 index 00000000..f633e9e0 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/DubboUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo; + +import com.alibaba.dubbo.rpc.Invocation; + +/** + * @author Eric Zhao + */ +public final class DubboUtils { + + public static final String DUBBO_APPLICATION_KEY = "dubboApplication"; + + public static String getApplication(Invocation invocation, String defaultValue) { + if (invocation == null || invocation.getAttachments() == null) { + throw new IllegalArgumentException("Bad invocation instance"); + } + return invocation.getAttachment(DUBBO_APPLICATION_KEY, defaultValue); + } + + private DubboUtils() {} +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java new file mode 100755 index 00000000..2e388545 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboConsumerFilter.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; +import com.alibaba.dubbo.common.extension.Activate; +import com.alibaba.dubbo.rpc.Filter; +import com.alibaba.dubbo.rpc.Invocation; +import com.alibaba.dubbo.rpc.Invoker; +import com.alibaba.dubbo.rpc.Result; +import com.alibaba.dubbo.rpc.RpcException; + +/** + *

Dubbo service consumer filter for Sentinel. Auto activated by default.

+ * + * If you want to disable the consumer filter, you can configure: + *
+ * <dubbo:consumer filter="-sentinel.dubbo.consumer.filter"/>
+ * 
+ * + * @author leyou + */ +@Activate(group = "consumer") +public class SentinelDubboConsumerFilter extends AbstractDubboFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + Entry interfaceEntry = null; + Entry methodEntry = null; + try { + String resourceName = getResourceName(invoker, invocation); + ContextUtil.enter(resourceName); + interfaceEntry = SphU.entry(invoker.getInterface().getName(), EntryType.OUT); + methodEntry = SphU.entry(resourceName, EntryType.OUT); + return invoker.invoke(invocation); + } catch (BlockException e) { + throw new SentinelRpcException(e); + } catch (RpcException e) { + Tracer.trace(e); + throw e; + } finally { + if (methodEntry != null) { + methodEntry.exit(); + } + if (interfaceEntry != null) { + interfaceEntry.exit(); + } + ContextUtil.exit(); + } + } +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java new file mode 100755 index 00000000..8f71018c --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/dubbo/SentinelDubboProviderFilter.java @@ -0,0 +1,75 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; +import com.alibaba.dubbo.common.extension.Activate; +import com.alibaba.dubbo.rpc.Filter; +import com.alibaba.dubbo.rpc.Invocation; +import com.alibaba.dubbo.rpc.Invoker; +import com.alibaba.dubbo.rpc.Result; +import com.alibaba.dubbo.rpc.RpcException; + +/** + *

Dubbo service provider filter for Sentinel. Auto activated by default.

+ * + * If you want to disable the provider filter, you can configure: + *
+ * <dubbo:provider filter="-sentinel.dubbo.provider.filter"/>
+ * 
+ * + * @author leyou + */ +@Activate(group = "provider") +public class SentinelDubboProviderFilter extends AbstractDubboFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + // Get origin caller. + String application = DubboUtils.getApplication(invocation, ""); + + Entry interfaceEntry = null; + Entry methodEntry = null; + try { + String resourceName = getResourceName(invoker, invocation); + String interfaceName = invoker.getInterface().getName(); + ContextUtil.enter(resourceName, application); + interfaceEntry = SphU.entry(interfaceName, EntryType.IN); + methodEntry = SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments()); + + return invoker.invoke(invocation); + } catch (BlockException e) { + throw new SentinelRpcException(e); + } catch (RpcException e) { + Tracer.trace(e); + throw e; + } finally { + if (methodEntry != null) { + methodEntry.exit(1, invocation.getArguments()); + } + if (interfaceEntry != null) { + interfaceEntry.exit(); + } + ContextUtil.exit(); + } + } +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter b/sentinel-adapter/sentinel-dubbo-adapter/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter new file mode 100755 index 00000000..292a4b25 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter @@ -0,0 +1,3 @@ +sentinel.dubbo.provider.filter=com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboProviderFilter +sentinel.dubbo.consumer.filter=com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboConsumerFilter +dubbo.application.context.name.filter=com.alibaba.csp.sentinel.adapter.dubbo.DubboAppContextFilter diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DemoService.java b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DemoService.java new file mode 100755 index 00000000..882de9ef --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/DemoService.java @@ -0,0 +1,23 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo; + +/** + * @author leyou + */ +public interface DemoService { + String sayHello(String name, int n); +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoServiceImpl.java b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoServiceImpl.java new file mode 100755 index 00000000..b5dfd930 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/dubbo/provider/DemoServiceImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.dubbo.provider; + +import com.alibaba.csp.sentinel.adapter.dubbo.DemoService; + +/** + * @author leyou + */ +public class DemoServiceImpl implements DemoService { + public String sayHello(String name, int n) { + return "Hello " + name + ", " + n; + } +} diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-consumer-filter.xml b/sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-consumer-filter.xml new file mode 100755 index 00000000..aef82044 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-consumer-filter.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-provider-filter.xml b/sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-provider-filter.xml new file mode 100755 index 00000000..95e3eca5 --- /dev/null +++ b/sentinel-adapter/sentinel-dubbo-adapter/src/test/resources/spring-dubbo-provider-filter.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-grpc-adapter/README.md b/sentinel-adapter/sentinel-grpc-adapter/README.md new file mode 100755 index 00000000..103f9927 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/README.md @@ -0,0 +1,38 @@ +# Sentinel gRPC Adapter + +Sentinel gRPC Adapter provides client and server interceptor for gRPC services. + +> Note that currently the interceptor only supports unary methods in gRPC. + In some circumstances (e.g. asynchronous call), the RT metrics might not be accurate. + +## Client Interceptor + +Example: + +```java +public class ServiceClient { + + private final ManagedChannel channel; + + ServiceClient(String host, int port) { + this.channel = ManagedChannelBuilder.forAddress(host, port) + .intercept(new SentinelGrpcClientInterceptor()) // Add the client interceptor. + .build(); + // Init your stub here. + } +} +``` + +## Server Interceptor + +Example: + +```java +import io.grpc.Server; + +Server server = ServerBuilder.forPort(port) + .addService(new MyServiceImpl()) // Add your service. + .intercept(new SentinelGrpcServerInterceptor()) // Add the server interceptor. + .build(); +``` + diff --git a/sentinel-adapter/sentinel-grpc-adapter/pom.xml b/sentinel-adapter/sentinel-grpc-adapter/pom.xml new file mode 100755 index 00000000..e5928c60 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/pom.xml @@ -0,0 +1,87 @@ + + + + sentinel-adapter + com.alibaba.csp + 0.1.0 + + 4.0.0 + sentinel-grpc-adapter + jar + + + + com.alibaba.csp + sentinel-core + + + + io.grpc + grpc-netty + ${grpc.version} + provided + + + io.grpc + grpc-protobuf + ${grpc.version} + provided + + + io.grpc + grpc-stub + ${grpc.version} + provided + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + com.alibaba + fastjson + ${fastjson.version} + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.5.0.Final + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.5.1 + + com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + + test-compile + test-compile-custom + + + + + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java new file mode 100755 index 00000000..47771388 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java @@ -0,0 +1,141 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.grpc; + +import javax.annotation.Nullable; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; + +/** + *

gRPC client interceptor for Sentinel. Currently it only works with unary methods.

+ * + * Example code: + *
+ * public class ServiceClient {
+ *
+ *     private final ManagedChannel channel;
+ *
+ *     ServiceClient(String host, int port) {
+ *         this.channel = ManagedChannelBuilder.forAddress(host, port)
+ *             .intercept(new SentinelGrpcClientInterceptor()) // Add the client interceptor.
+ *             .build();
+ *         // Init your stub here.
+ *     }
+ *
+ * }
+ * 
+ * + * For server interceptor, see {@link SentinelGrpcServerInterceptor}. + * + * @author Eric Zhao + */ +public class SentinelGrpcClientInterceptor implements ClientInterceptor { + + private static final Status FLOW_CONTROL_BLOCK = Status.UNAVAILABLE.withDescription( + "Flow control limit exceeded (client side)"); + + @Override + public ClientCall interceptCall(MethodDescriptor methodDescriptor, + CallOptions callOptions, Channel channel) { + String resourceName = methodDescriptor.getFullMethodName(); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, EntryType.OUT); + // Allow access, forward the call. + return new ForwardingClientCall.SimpleForwardingClientCall( + channel.newCall(methodDescriptor, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start(new SimpleForwardingClientCallListener(responseListener) { + @Override + public void onReady() { + super.onReady(); + } + + @Override + public void onClose(Status status, Metadata trailers) { + super.onClose(status, trailers); + // Record the exception metrics. + if (!status.isOk()) { + recordException(status.asRuntimeException()); + } + } + }, headers); + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + super.cancel(message, cause); + // Record the exception metrics. + recordException(cause); + } + }; + } catch (BlockException e) { + // Flow control threshold exceeded, block the call. + return new ClientCall() { + @Override + public void start(Listener responseListener, Metadata headers) { + responseListener.onClose(FLOW_CONTROL_BLOCK, new Metadata()); + } + + @Override + public void request(int numMessages) { + + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + + } + + @Override + public void halfClose() { + + } + + @Override + public void sendMessage(ReqT message) { + + } + }; + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + private void recordException(Throwable t) { + Tracer.trace(t); + } +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java new file mode 100755 index 00000000..1e0d9090 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java @@ -0,0 +1,90 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.grpc; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import io.grpc.ForwardingServerCall; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; + +/** + *

gRPC server interceptor for Sentinel. Currently it only works with unary methods.

+ * + * Example code: + *
+ * Server server = ServerBuilder.forPort(port)
+ *      .addService(new MyServiceImpl()) // Add your service.
+ *      .intercept(new SentinelGrpcServerInterceptor()) // Add the server interceptor.
+ *      .build();
+ * 
+ * + * For client interceptor, see {@link SentinelGrpcClientInterceptor}. + * + * @author Eric Zhao + */ +public class SentinelGrpcServerInterceptor implements ServerInterceptor { + + private static final Status FLOW_CONTROL_BLOCK = Status.UNAVAILABLE.withDescription( + "Flow control limit exceeded (server side)"); + + @Override + public Listener interceptCall(ServerCall serverCall, Metadata metadata, + ServerCallHandler serverCallHandler) { + String resourceName = serverCall.getMethodDescriptor().getFullMethodName(); + // Remote address: serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, EntryType.IN); + // Allow access, forward the call. + return new ForwardingServerCallListener.SimpleForwardingServerCallListener( + serverCallHandler.startCall( + new ForwardingServerCall.SimpleForwardingServerCall(serverCall) { + @Override + public void close(Status status, Metadata trailers) { + super.close(status, trailers); + // Record the exception metrics. + if (!status.isOk()) { + recordException(status.asRuntimeException()); + } + } + }, metadata)) {}; + } catch (BlockException e) { + serverCall.close(FLOW_CONTROL_BLOCK, new Metadata()); + return new ServerCall.Listener() {}; + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + private void recordException(Throwable t) { + Tracer.trace(t); + } +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java new file mode 100755 index 00000000..dab36678 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.grpc; + +import java.util.concurrent.TimeUnit; + +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooServiceGrpc; + +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; + +/** + * A simple wrapped gRPC client for FooService. + * + * @author Eric Zhao + */ +final class FooServiceClient { + + private final ManagedChannel channel; + private final FooServiceGrpc.FooServiceBlockingStub blockingStub; + + FooServiceClient(String host, int port) { + this.channel = ManagedChannelBuilder.forAddress(host, port) + .usePlaintext() + .build(); + this.blockingStub = FooServiceGrpc.newBlockingStub(this.channel); + } + + FooServiceClient(String host, int port, ClientInterceptor interceptor) { + this.channel = ManagedChannelBuilder.forAddress(host, port) + .usePlaintext() + .intercept(interceptor) + .build(); + this.blockingStub = FooServiceGrpc.newBlockingStub(this.channel); + } + + FooResponse sayHello(FooRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + return blockingStub.sayHello(request); + } + + FooResponse anotherHello(FooRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + return blockingStub.anotherHello(request); + } + + void shutdown() throws InterruptedException { + channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); + } +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java new file mode 100755 index 00000000..adda5102 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.grpc; + +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooServiceGrpc; + +import io.grpc.stub.StreamObserver; + +/** + * Implementation of FooService defined in proto. + */ +class FooServiceImpl extends FooServiceGrpc.FooServiceImplBase { + + @Override + public void sayHello(FooRequest request, StreamObserver responseObserver) { + String message = String.format("Hello %s! Your ID is %d.", request.getName(), request.getId()); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + @Override + public void anotherHello(FooRequest request, StreamObserver responseObserver) { + String message = String.format("Good day, %s (%d)", request.getName(), request.getId()); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java new file mode 100755 index 00000000..b7acc649 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.grpc; + +import java.io.IOException; +import java.util.Collections; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.StatusRuntimeException; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link SentinelGrpcClientInterceptor}. + * + * @author Eric Zhao + */ +public class SentinelGrpcClientInterceptorTest { + + private final String resourceName = "com.alibaba.sentinel.examples.FooService/sayHello"; + private final int threshold = 2; + + private Server server; + + private void configureFlowRule() { + FlowRule rule = new FlowRule() + .setCount(threshold) + .setGrade(RuleConstant.FLOW_GRADE_QPS) + .setResource(resourceName) + .setLimitApp("default") + .as(FlowRule.class); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + //@Test + public void testGrpcClientInterceptor() throws Exception { + final int port = 19328; + + configureFlowRule(); + prepareServer(port); + + FooServiceClient client = new FooServiceClient("localhost", port, new SentinelGrpcClientInterceptor()); + final int total = 8; + for (int i = 0; i < total; i++) { + sendRequest(client); + } + ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.OUT); + assertNotNull(clusterNode); + + assertEquals((total - threshold) / 2, clusterNode.blockedRequest()); + assertEquals(total / 2, clusterNode.totalRequest()); + + long totalQps = clusterNode.totalQps(); + long passQps = clusterNode.passQps(); + long blockedQps = clusterNode.blockedQps(); + assertEquals(total, totalQps); + assertEquals(total - threshold, blockedQps); + assertEquals(threshold, passQps); + + stopServer(); + } + + private void sendRequest(FooServiceClient client) { + try { + FooResponse response = client.sayHello(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); + System.out.println(ClusterBuilderSlot.getClusterNode(resourceName, EntryType.OUT).avgRt()); + System.out.println("Response: " + response); + } catch (StatusRuntimeException ex) { + System.out.println("Blocked, cause: " + ex.getMessage()); + } + } + + private void prepareServer(int port) throws IOException { + if (server != null) { + throw new IllegalStateException("Server already running!"); + } + server = ServerBuilder.forPort(port) + .addService(new FooServiceImpl()) + .build(); + server.start(); + } + + private void stopServer() { + if (server != null) { + server.shutdown(); + server = null; + } + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java new file mode 100755 index 00000000..5be16eee --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.grpc; + +import java.io.IOException; +import java.util.Collections; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.StatusRuntimeException; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link SentinelGrpcServerInterceptor}. + * + * @author Eric Zhao + */ +public class SentinelGrpcServerInterceptorTest { + + private final String resourceName = "com.alibaba.sentinel.examples.FooService/anotherHello"; + private final int threshold = 4; + + private Server server; + private FooServiceClient client; + + private void configureFlowRule() { + FlowRule rule = new FlowRule() + .setCount(threshold) + .setGrade(RuleConstant.FLOW_GRADE_QPS) + .setResource(resourceName) + .setLimitApp("default") + .as(FlowRule.class); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + //@Test + public void testGrpcServerInterceptor() throws Exception { + final int port = 19329; + client = new FooServiceClient("localhost", port); + + configureFlowRule(); + prepareServer(port); + + final int total = 8; + for (int i = 0; i < total; i++) { + sendRequest(); + } + ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.IN); + assertNotNull(clusterNode); + + assertEquals((total - threshold) / 2, clusterNode.blockedRequest()); + assertEquals(total / 2, clusterNode.totalRequest()); + + long totalQps = clusterNode.totalQps(); + long passQps = clusterNode.passQps(); + long blockedQps = clusterNode.blockedQps(); + assertEquals(total, totalQps); + assertEquals(total - threshold, blockedQps); + assertEquals(threshold, passQps); + + stopServer(); + } + + private void sendRequest() { + try { + FooResponse response = client.anotherHello(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); + System.out.println("Response: " + response); + } catch (StatusRuntimeException ex) { + System.out.println("Blocked, cause: " + ex.getMessage()); + } + } + + private void prepareServer(int port) throws IOException { + if (server != null) { + throw new IllegalStateException("Server already running!"); + } + server = ServerBuilder.forPort(port) + .addService(new FooServiceImpl()) + .intercept(new SentinelGrpcServerInterceptor()) + .build(); + server.start(); + } + + private void stopServer() { + if (server != null) { + server.shutdown(); + server = null; + } + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto b/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto new file mode 100755 index 00000000..61858510 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "com.alibaba.csp.sentinel.adapter.grpc.gen"; +option java_outer_classname = "FooProto"; + +package com.alibaba.sentinel.examples; + +message FooRequest { + string name = 1; + int32 id = 2; +} + +message FooResponse { + string message = 1; +} + +// Example service definition. +service FooService { + rpc sayHello(FooRequest) returns (FooResponse) {} + + rpc anotherHello(FooRequest) returns (FooResponse) {} +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-boot-starter/README.md b/sentinel-adapter/sentinel-spring-boot-starter/README.md new file mode 100755 index 00000000..83560f54 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/README.md @@ -0,0 +1,18 @@ +# Sentinel Spring Boot Starter + +Sentinel Spring Boot Starter provides out-of-box integration with Spring Boot applications +(e.g. web applications, Dubbo services). + +## Web Servlet + +Web servlet integration is enabled by default. You need to configure URL patterns in your config file (e.g. properties file): + +``` +spring.sentinel.servletFilter.urlPatterns=/* +``` + +By default the URL pattern is `/*`. + +## Dubbo + +Dubbo integration is enabled by default. You need to disable the filters manually if you don't want them. \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-boot-starter/pom.xml b/sentinel-adapter/sentinel-spring-boot-starter/pom.xml new file mode 100755 index 00000000..dc04ded5 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/pom.xml @@ -0,0 +1,95 @@ + + + + sentinel-adapter + com.alibaba.csp + 0.1.0 + + 4.0.0 + jar + + sentinel-spring-boot-starter + + + 1.5.14.RELEASE + + + + + com.alibaba.csp + sentinel-core + ${project.version} + + + com.alibaba.csp + sentinel-transport-simple-http + ${project.version} + + + com.alibaba.csp + sentinel-web-servlet + ${project.version} + + + com.alibaba.csp + sentinel-dubbo-adapter + ${project.version} + + + + org.slf4j + slf4j-api + 1.7.25 + compile + + + + org.springframework.boot + spring-boot-starter + ${spring.boot.version} + compile + + + org.springframework.boot + spring-boot-autoconfigure + ${spring.boot.version} + compile + + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + compile + true + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + provided + true + + + org.springframework.boot + spring-boot-actuator + ${spring.boot.version} + compile + true + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + junit + junit + ${junit.version} + test + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/Constants.java b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/Constants.java new file mode 100755 index 00000000..bfb98e05 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/Constants.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.boot.sentinel; + +/** + * @author Eric Zhao + */ +public final class Constants { + + /** + * Property prefix in application.properties. + */ + public static final String PREFIX = "spring.sentinel"; + public static final String SENTINEL_SERVLET_ENABLED = "spring.sentinel.servletFilter.enabled"; + public static final String SENTINEL_ENABLED = "spring.sentinel.enabled"; + + private Constants() {} +} diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/config/SentinelWebServletAutoConfiguration.java b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/config/SentinelWebServletAutoConfiguration.java new file mode 100755 index 00000000..f996fbeb --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/config/SentinelWebServletAutoConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.boot.sentinel.config; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.Filter; + +import com.alibaba.boot.sentinel.Constants; +import com.alibaba.boot.sentinel.property.SentinelProperties; +import com.alibaba.boot.sentinel.property.SentinelProperties.ServletFilterConfig; +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.CollectionUtils; + +/** + * Auto configuration for Sentinel web servlet filter. + * + * @author Eric Zhao + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnProperty(name = {Constants.SENTINEL_ENABLED, Constants.SENTINEL_SERVLET_ENABLED}, matchIfMissing = true) +@EnableConfigurationProperties(SentinelProperties.class) +public class SentinelWebServletAutoConfiguration { + + private final Logger logger = LoggerFactory.getLogger(SentinelWebServletAutoConfiguration.class); + + @Autowired + private SentinelProperties properties; + + @Bean + @ConditionalOnWebApplication + public FilterRegistrationBean sentinelFilterRegistrationBean() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + if (!properties.isEnabled()) { + return registrationBean; + } + + ServletFilterConfig filterConfig = properties.getServletFilter(); + if (null == filterConfig) { + filterConfig = new ServletFilterConfig(); + properties.setServletFilter(filterConfig); + } + + if (CollectionUtils.isEmpty(filterConfig.getUrlPatterns())) { + List defaultPatterns = new ArrayList(); + defaultPatterns.add("/*"); + filterConfig.setUrlPatterns(defaultPatterns); + logger.info("[Sentinel Starter] Using default patterns for web servlet filter: {}", defaultPatterns); + } + registrationBean.addUrlPatterns(filterConfig.getUrlPatterns().toArray(new String[0])); + + Filter filter = new CommonFilter(); + registrationBean.setFilter(filter); + registrationBean.setOrder(filterConfig.getOrder()); + + logger.info("[Sentinel Starter] Web servlet filter registered with urlPatterns: {}", + filterConfig.getUrlPatterns()); + return registrationBean; + } +} diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelActuatorEndpoint.java b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelActuatorEndpoint.java new file mode 100755 index 00000000..7dd14ae5 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelActuatorEndpoint.java @@ -0,0 +1,68 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.boot.sentinel.endpoint; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.boot.sentinel.property.SentinelProperties; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.mvc.AbstractMvcEndpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author duanling + * @author Eric Zhao + */ +@ConfigurationProperties("endpoints.sentinel") +public class SentinelActuatorEndpoint extends AbstractMvcEndpoint { + + @Autowired + private SentinelProperties sentinelProperties; + + public SentinelActuatorEndpoint() { + super("/sentinel", false); + } + + @RequestMapping + @ResponseBody + public Map invoke() { + Map result = new HashMap(); + + result.put("version", this.getClass().getPackage().getImplementationVersion()); + result.put("properties", sentinelProperties); + + List flowRules = FlowRuleManager.getRules(); + List degradeRules = DegradeRuleManager.getRules(); + List systemRules = SystemRuleManager.getRules(); + + result.put("flowRules", flowRules); + result.put("degradeRules", degradeRules); + result.put("systemRules", systemRules); + + return result; + } +} diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelEndpointManagementContextConfiguration.java b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelEndpointManagementContextConfiguration.java new file mode 100755 index 00000000..3f1054ae --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/endpoint/SentinelEndpointManagementContextConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.boot.sentinel.endpoint; + +import com.alibaba.boot.sentinel.property.SentinelProperties; + +import org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration; +import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * @author duanling + */ +@ManagementContextConfiguration +@EnableConfigurationProperties({SentinelProperties.class}) +public class SentinelEndpointManagementContextConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint("sentinel") + public SentinelActuatorEndpoint sentinelEndPoint() { + return new SentinelActuatorEndpoint(); + } +} diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/property/SentinelProperties.java b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/property/SentinelProperties.java new file mode 100755 index 00000000..bdd281d2 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/java/com/alibaba/boot/sentinel/property/SentinelProperties.java @@ -0,0 +1,93 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.boot.sentinel.property; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.Ordered; + +/** + * @author Eric Zhao + */ +@ConfigurationProperties(prefix = "spring.sentinel") +public class SentinelProperties { + + private boolean enabled = true; + + private ServletFilterConfig servletFilter; + + public boolean isEnabled() { + return enabled; + } + + public SentinelProperties setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public ServletFilterConfig getServletFilter() { + return servletFilter; + } + + public SentinelProperties setServletFilter( + ServletFilterConfig servletFilter) { + this.servletFilter = servletFilter; + return this; + } + + public static class DubboFilterConfig {} + + public static class ServletFilterConfig { + + private boolean enabled = true; + + /** + * Chain order for Sentinel servlet filter. + */ + private int order = Ordered.HIGHEST_PRECEDENCE; + + /** + * URL pattern for Sentinel servlet filter. + */ + private List urlPatterns; + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + public List getUrlPatterns() { + return urlPatterns; + } + + public void setUrlPatterns(List urlPatterns) { + this.urlPatterns = urlPatterns; + } + + public boolean isEnabled() { + return enabled; + } + + public ServletFilterConfig setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + } +} diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.factories b/sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100755 index 00000000..15992e33 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.boot.sentinel.config.SentinelWebServletAutoConfiguration + +org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\ +com.alibaba.boot.sentinel.endpoint.SentinelEndpointManagementContextConfiguration \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.provides b/sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.provides new file mode 100755 index 00000000..10a1f7ae --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/main/resources/META-INF/spring.provides @@ -0,0 +1 @@ +provides: sentinel-core \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/test/java/com/alibaba/boot/sentinel/SimpleWebApplication.java b/sentinel-adapter/sentinel-spring-boot-starter/src/test/java/com/alibaba/boot/sentinel/SimpleWebApplication.java new file mode 100755 index 00000000..572b02c2 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/test/java/com/alibaba/boot/sentinel/SimpleWebApplication.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.boot.sentinel; + +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Eric Zhao + */ +@SpringBootApplication +@RestController +@PropertySource("classpath:web-servlet.properties") +public class SimpleWebApplication { + + @RequestMapping("/foo") + public String foo() { + return "Hello!"; + } + + @RequestMapping("/baz") + public String baz() { + ClusterNode node = ClusterBuilderSlot.getClusterNode("/foo"); + if (node == null) { + return "/foo has not been called!"; + } else { + return "/foo total request in metrics: " + node.totalRequest(); + } + } + + public static void main(String[] args) { + SpringApplication.run(SimpleWebApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-spring-boot-starter/src/test/resources/web-servlet.properties b/sentinel-adapter/sentinel-spring-boot-starter/src/test/resources/web-servlet.properties new file mode 100755 index 00000000..41eb21a2 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-boot-starter/src/test/resources/web-servlet.properties @@ -0,0 +1,3 @@ +spring.sentinel.enabled=true +spring.sentinel.servletFilter.enabled=true +spring.sentinel.servletFilter.urlPatterns=/foo \ No newline at end of file diff --git a/sentinel-adapter/sentinel-web-servlet/README.md b/sentinel-adapter/sentinel-web-servlet/README.md new file mode 100755 index 00000000..39bfefc2 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/README.md @@ -0,0 +1,22 @@ +# Sentinel Web Servlet Filter + +Sentinel provides Servlet filter integration. To use the filter, +you can simply configure your `web.xml` with: + +```xml + + SentinelCommonFilter + com.alibaba.csp.sentinel.adapter.servlet.CommonFilter + + + + SentinelCommonFilter + /* + +``` + +When a request is blocked, Sentinel servlet filter will give a default page indicating the request blocked. +If customized block page is set (via `WebServletConfig.setBlockPage(blockPage)` method), +the filter will redirect the request to provided URL. You can also implement your own +block handler (the `UrlBlockHandler` interface) and register to `WebCallbackManager`. + diff --git a/sentinel-adapter/sentinel-web-servlet/pom.xml b/sentinel-adapter/sentinel-web-servlet/pom.xml new file mode 100755 index 00000000..5f253447 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-adapter + 0.1.0 + + sentinel-web-servlet + jar + + + com.alibaba.csp + sentinel-core + + + javax.servlet + javax.servlet-api + provided + + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java new file mode 100755 index 00000000..7ca3fe27 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * Servlet filter that integrates with Sentinel. + * + * @author youji.zj + */ +public class CommonFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest)request; + Entry entry = null; + + try { + String target = FilterUtil.filterTarget(sRequest); + target = WebCallbackManager.getUrlCleaner().clean(target); + + ContextUtil.enter(target); + entry = SphU.entry(target, EntryType.IN); + + chain.doFilter(request, response); + } catch (BlockException e) { + HttpServletResponse sResponse = (HttpServletResponse)response; + WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse); + } catch (IOException e2) { + Tracer.trace(e2); + throw e2; + } catch (ServletException e3) { + Tracer.trace(e3); + throw e3; + } catch (RuntimeException e4) { + Tracer.trace(e4); + throw e4; + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + @Override + public void destroy() { + + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java new file mode 100755 index 00000000..4881df46 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * Servlet filter for all requests. + * + * @author youji.zj + */ +public class CommonTotalFilter implements Filter { + + public static final String TOTAL_URL_REQUEST = "total-url-request"; + + @Override + public void init(FilterConfig filterConfig) { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest)request; + String target = FilterUtil.filterTarget(sRequest); + target = WebCallbackManager.getUrlCleaner().clean(target); + + Entry entry = null; + try { + ContextUtil.enter(target); + entry = SphU.entry(TOTAL_URL_REQUEST); + chain.doFilter(request, response); + } catch (BlockException e) { + HttpServletResponse sResponse = (HttpServletResponse)response; + WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse); + } catch (IOException e2) { + Tracer.trace(e2); + throw e2; + } catch (ServletException e3) { + Tracer.trace(e3); + throw e3; + } catch (RuntimeException e4) { + Tracer.trace(e4); + throw e4; + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + @Override + public void destroy() { + + } + +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java new file mode 100755 index 00000000..cf14749f --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.callback; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; + +/*** + * The default {@link UrlBlockHandler}. + * + * @author youji.zj + */ +public class DefaultUrlBlockHandler implements UrlBlockHandler { + + @Override + public void blocked(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Directly redirect to the default flow control (blocked) page or customized block page. + FilterUtil.blockRequest(request, response); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java new file mode 100755 index 00000000..2d80abd4 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java @@ -0,0 +1,27 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.callback; + +/*** + * @author youji.zj + */ +public class DefaultUrlCleaner implements UrlCleaner { + + @Override + public String clean(String originUrl) { + return originUrl; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java new file mode 100755 index 00000000..dc6d1f86 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.callback; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/*** + * The URL block handler handles requests when blocked. + * + * @author youji.zj + */ +public interface UrlBlockHandler { + + /** + * Handle the request when blocked. + * + * @param request Servlet request + * @param response Servlet response + * @throws IOException some error occurs + */ + void blocked(HttpServletRequest request, HttpServletResponse response) throws IOException; +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java new file mode 100755 index 00000000..c5883396 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.callback; + +/*** + * @author youji.zj + */ +public interface UrlCleaner { + + /*** + *

Process the url. Some path variables should be handled and unified.

+ *

e.g. collect_item_relation--10200012121-.html will be converted to collect_item_relation.html

+ * + * @param originUrl original url + * @return processed url + */ + String clean(String originUrl); +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java new file mode 100755 index 00000000..435dd9db --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.callback; + +/** + * Registry for URL cleaner and URL block handler. + * + * @author youji.zj + */ +public class WebCallbackManager { + + /** + * URL cleaner + */ + private static volatile UrlCleaner urlCleaner = new DefaultUrlCleaner(); + + /** + * URL block handler + */ + private static volatile UrlBlockHandler urlBlockHandler = new DefaultUrlBlockHandler(); + + public static UrlCleaner getUrlCleaner() { + return urlCleaner; + } + + public static void setUrlCleaner(UrlCleaner urlCleaner) { + WebCallbackManager.urlCleaner = urlCleaner; + } + + public static UrlBlockHandler getUrlBlockHandler() { + return urlBlockHandler; + } + + public static void setUrlBlockHandler(UrlBlockHandler urlBlockHandler) { + WebCallbackManager.urlBlockHandler = urlBlockHandler; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java new file mode 100755 index 00000000..bcaafe07 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.config; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import com.alibaba.csp.sentinel.adapter.servlet.CommonTotalFilter; +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * @author leyou + */ +public class WebServletConfig { + + public static final String BLOCK_PAGE = "csp.sentinel.web.servlet.block.page"; + + /** + * Get redirecting page when Sentinel blocking for {@link CommonFilter} or + * {@link CommonTotalFilter} occurs. + * + * @return the block page URL, maybe null if not configured. + */ + public static String getBlockPage() { + return SentinelConfig.getConfig(BLOCK_PAGE); + } + + public static void setBlockPage(String blockPage) { + SentinelConfig.setConfig(BLOCK_PAGE, blockPage); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java new file mode 100755 index 00000000..ad3434c5 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java @@ -0,0 +1,185 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.adapter.servlet.util; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Util class for web servlet filter. + * + * @author youji.zj + * @author Eric Zhao + */ +public final class FilterUtil { + + public static String filterTarget(HttpServletRequest request) { + String pathInfo = getResourcePath(request); + if (!pathInfo.startsWith("/")) { + pathInfo = "/" + pathInfo; + } + + if ("/".equals(pathInfo)) { + return pathInfo; + } + + // Note: pathInfo should be converted to camelCase style. + int lastSlashIndex = pathInfo.lastIndexOf("/"); + + if (lastSlashIndex >= 0) { + pathInfo = pathInfo.substring(0, lastSlashIndex) + "/" + + StringUtil.trim(pathInfo.substring(lastSlashIndex + 1)); + } else { + pathInfo = "/" + StringUtil.trim(pathInfo); + } + + return pathInfo; + } + + public static void blockRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { + StringBuffer url = request.getRequestURL(); + + if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { + url.append("?").append(request.getQueryString()); + } + + if (StringUtil.isEmpty(WebServletConfig.getBlockPage())) { + writeDefaultBlockedPage(response); + } else { + String redirectUrl = WebServletConfig.getBlockPage() + "?http_referer=" + url.toString(); + // Redirect to the customized block page. + response.sendRedirect(redirectUrl); + } + } + + private static void writeDefaultBlockedPage(HttpServletResponse response) throws IOException { + PrintWriter out = response.getWriter(); + out.println("Blocked by Sentinel (flow limiting)"); + out.flush(); + out.close(); + } + + private static String getResourcePath(HttpServletRequest request) { + String pathInfo = normalizeAbsolutePath(request.getPathInfo(), false); + String servletPath = normalizeAbsolutePath(request.getServletPath(), pathInfo.length() != 0); + + return servletPath + pathInfo; + } + + private static String normalizeAbsolutePath(String path, boolean removeTrailingSlash) throws IllegalStateException { + return normalizePath(path, true, false, removeTrailingSlash); + } + + private static String normalizePath(String path, boolean forceAbsolute, boolean forceRelative, + boolean removeTrailingSlash) throws IllegalStateException { + char[] pathChars = StringUtil.trimToEmpty(path).toCharArray(); + int length = pathChars.length; + + // Check path and slash. + boolean startsWithSlash = false; + boolean endsWithSlash = false; + + if (length > 0) { + char firstChar = pathChars[0]; + char lastChar = pathChars[length - 1]; + + startsWithSlash = firstChar == '/' || firstChar == '\\'; + endsWithSlash = lastChar == '/' || lastChar == '\\'; + } + + StringBuilder buf = new StringBuilder(length); + boolean isAbsolutePath = forceAbsolute || !forceRelative && startsWithSlash; + int index = startsWithSlash ? 0 : -1; + int level = 0; + + if (isAbsolutePath) { + buf.append("/"); + } + + while (index < length) { + index = indexOfSlash(pathChars, index + 1, false); + + if (index == length) { + break; + } + + int nextSlashIndex = indexOfSlash(pathChars, index, true); + + String element = new String(pathChars, index, nextSlashIndex - index); + index = nextSlashIndex; + + // Ignore "." + if (".".equals(element)) { + continue; + } + + // Backtrack ".." + if ("..".equals(element)) { + if (level == 0) { + if (isAbsolutePath) { + throw new IllegalStateException(path); + } else { + buf.append("../"); + } + } else { + buf.setLength(pathChars[--level]); + } + + continue; + } + + pathChars[level++] = (char)buf.length(); + buf.append(element).append('/'); + } + + // remove the last "/" + if (buf.length() > 0) { + if (!endsWithSlash || removeTrailingSlash) { + buf.setLength(buf.length() - 1); + } + } + + return buf.toString(); + } + + private static int indexOfSlash(char[] chars, int beginIndex, boolean slash) { + int i = beginIndex; + + for (; i < chars.length; i++) { + char ch = chars[i]; + + if (slash) { + if (ch == '/' || ch == '\\') { + break; // if a slash + } + } else { + if (ch != '/' && ch != '\\') { + break; // if not a slash + } + } + } + + return i; + } + + private FilterUtil() {} +} diff --git a/sentinel-core/pom.xml b/sentinel-core/pom.xml new file mode 100755 index 00000000..1bccb832 --- /dev/null +++ b/sentinel-core/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-parent + 0.1.0 + + sentinel-core + jar + The core of Sentinel + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java new file mode 100755 index 00000000..311f932b --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Constants.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +/** + * @author qinan.qn + * @author youji.zj + * @author jialiang.linjl + */ +public class Constants { + + public final static int MAX_CONTEXT_NAME_SIZE = 2000; + public final static int MAX_SLOT_CHAIN_SIZE = 6000; + public final static String ROOT_ID = "machine-root"; + public final static String CONTEXT_DEFAULT_NAME = "default_context_name"; + + public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN), + Env.nodeBuilder.buildClusterNode()); + + /** + * statistics for {@link SystemRule} checking. + */ + public final static ClusterNode ENTRY_NODE = new ClusterNode(); + + /** + * 超过这个时间的请求不作为平均时间计算 + */ + public final static int TIME_DROP_VALVE = 4900; + + /*** 框架功能打开或者关闭的开关 ***/ + public static volatile boolean ON = true; + +} \ No newline at end of file diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java new file mode 100755 index 00000000..21d178d6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java @@ -0,0 +1,279 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.context.NullContext; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slotchain.MethodResourceWrapper; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.Rule; + +/** + * {@inheritDoc} + * + * @author jialiang.linjl + * @author leyou(lihao) + * @author Eric Zhao + * @see Sph + */ +public class CtSph implements Sph { + + private static final Object[] OBJECTS0 = new Object[0]; + + /** + * Same resource({@link ResourceWrapper#equals(Object)}) will share the same + * {@link ProcessorSlotChain}, no matter in which {@link Context}. + */ + private static Map chainMap + = new HashMap(); + + private static final Object LOCK = new Object(); + + /** + * Do all {@link Rule}s checking about the resource. + * + *

Each distinct resource will use a {@link ProcessorSlot} to do rules checking. Same resource will use + * same {@link ProcessorSlot} globally.

+ * + *

Note that total {@link ProcessorSlot} count must not exceed {@link Constants#MAX_SLOT_CHAIN_SIZE}, + * otherwise no rules checking will do. In this condition, all requests will pass directly, with no checking + * or exception.

+ * + * @param resourceWrapper resource name + * @param count tokens needed + * @param args arguments of user method call + * @return {@link Entry} represents this call + * @throws BlockException if any rule's threshold is exceeded + */ + public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { + Context context = ContextUtil.getContext(); + if (context instanceof NullContext) { + // Init the entry only. No rule checking will occur. + return new CtEntry(resourceWrapper, null, context); + } + + if (context == null) { + context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType()); + } + + // Global switch is close, no rule checking will do. + if (!Constants.ON) { + return new CtEntry(resourceWrapper, null, context); + } + + ProcessorSlot chain = lookProcessChain(resourceWrapper); + + /* + * Means processor size exceeds {@link Constants.MAX_ENTRY_SIZE}, no + * rule checking will do. + */ + if (chain == null) { + return new CtEntry(resourceWrapper, null, context); + } + + Entry e = new CtEntry(resourceWrapper, chain, context); + try { + chain.entry(context, resourceWrapper, null, count, args); + } catch (BlockException e1) { + e.exit(count, args); + throw e1; + } catch (Throwable e1) { + RecordLog.info("sentinel unexpected exception", e1); + } + return e; + } + + /** + * Get {@link ProcessorSlotChain} of the resource. new {@link ProcessorSlotChain} will + * be created if the resource doesn't relate one. + * + *

Same resource({@link ResourceWrapper#equals(Object)}) will share the same + * {@link ProcessorSlotChain} globally, no matter in witch {@link Context}.

+ * + *

+ * Note that total {@link ProcessorSlot} count must not exceed {@link Constants#MAX_SLOT_CHAIN_SIZE}, + * otherwise null will return. + *

+ * + * @param resourceWrapper target resource + * @return {@link ProcessorSlotChain} of the resource + */ + private ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) { + ProcessorSlotChain chain = chainMap.get(resourceWrapper); + if (chain == null) { + synchronized (LOCK) { + chain = chainMap.get(resourceWrapper); + if (chain == null) { + // Entry size limit. + if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { + return null; + } + + chain = Env.slotsChainbuilder.build(); + HashMap newMap + = new HashMap( + chainMap.size() + 1); + newMap.putAll(chainMap); + newMap.put(resourceWrapper, chain); + chainMap = newMap; + } + } + } + return chain; + } + + private static class CtEntry extends Entry { + + protected Entry parent = null; + protected Entry child = null; + private ProcessorSlot chain; + private Context context; + + CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot chain, Context context) { + super(resourceWrapper); + this.chain = chain; + this.context = context; + parent = context.getCurEntry(); + if (parent != null) { + ((CtEntry)parent).child = this; + } + context.setCurEntry(this); + } + + @Override + public void exit(int count, Object... args) throws ErrorEntryFreeException { + trueExit(count, args); + } + + @Override + protected Entry trueExit(int count, Object... args) throws ErrorEntryFreeException { + if (context != null) { + if (context.getCurEntry() != this) { + // Clean previous call stack. + CtEntry e = (CtEntry)context.getCurEntry(); + while (e != null) { + e.exit(count, args); + e = (CtEntry)e.parent; + } + throw new ErrorEntryFreeException( + "The order of entry free is can't be paired with the order of entry"); + } else { + if (chain != null) { + chain.exit(context, resourceWrapper, count, args); + } + // Modify the call stack. + context.setCurEntry(parent); + if (parent != null) { + ((CtEntry)parent).child = null; + } + if (parent == null) { + // Auto-created entry indicates immediate exit. + ContextUtil.exit(); + } + // Clean the reference of context in current entry to avoid duplicate exit. + context = null; + } + } + return parent; + + } + + @Override + public Node getLastNode() { + return parent == null ? null : parent.getCurNode(); + } + } + + /** + * This class is used for skip context name checking. + */ + private final static class MyContextUtil extends ContextUtil { + static Context myEnter(String name, String origin, EntryType type) { + return trueEnter(name, origin); + } + } + + @Override + public Entry entry(String name) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, EntryType.OUT); + return entry(resource, 1, OBJECTS0); + } + + @Override + public Entry entry(Method method) throws BlockException { + MethodResourceWrapper resource = new MethodResourceWrapper(method, EntryType.OUT); + return entry(resource, 1, OBJECTS0); + } + + @Override + public Entry entry(Method method, EntryType type) throws BlockException { + MethodResourceWrapper resource = new MethodResourceWrapper(method, type); + return entry(resource, 1, OBJECTS0); + } + + @Override + public Entry entry(String name, EntryType type) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, type); + return entry(resource, 1, OBJECTS0); + } + + @Override + public Entry entry(Method method, EntryType type, int count) throws BlockException { + MethodResourceWrapper resource = new MethodResourceWrapper(method, type); + return entry(resource, count, OBJECTS0); + } + + @Override + public Entry entry(String name, EntryType type, int count) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, type); + return entry(resource, count, OBJECTS0); + } + + @Override + public Entry entry(Method method, int count) throws BlockException { + MethodResourceWrapper resource = new MethodResourceWrapper(method, EntryType.OUT); + return entry(resource, count, OBJECTS0); + } + + @Override + public Entry entry(String name, int count) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, EntryType.OUT); + return entry(resource, count, OBJECTS0); + } + + @Override + public Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException { + MethodResourceWrapper resource = new MethodResourceWrapper(method, type); + return entry(resource, count, args); + } + + @Override + public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException { + StringResourceWrapper resource = new StringResourceWrapper(name, type); + return entry(resource, count, args); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Entry.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Entry.java new file mode 100755 index 00000000..1c1f0cf1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Entry.java @@ -0,0 +1,141 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.context.Context; + +/** + * Each {@link SphU}#entry() will return an {@link Entry}. This class holds information of current invocation:
+ * + *
    + *
  • createTime, the create time of this entry, using for rt statistics.
  • + *
  • current {@link Node}, that is statistics of the resource in current context.
  • + *
  • origin {@link Node}, that is statistics for the specific origin. Usually the + * origin could be the Service Consumer's app name, see + * {@link ContextUtil#enter(String name, String origin)}
  • + *
  • {@link ResourceWrapper}, that is resource name.
  • + *
    + *
+ * + *

+ * A invocation tree will be created if we invoke SphU#entry() multi times in the same {@link Context}, + * so parent or child entry may be held by this to form the tree. Since {@link Context} always holds + * the current entry in the invocation tree, every {@link Entry#exit()} call should modify + * {@link Context#setCurEntry(Entry)} as parent entry of this. + *

+ * + * @author qinan.qn + * @author jialiang.linjl + * @author leyou(lihao) + * @see SphU + * @see Context + * @see ContextUtil + */ +public abstract class Entry { + + private static final Object[] OBJECTS0 = new Object[0]; + + private long createTime; + private Node curNode; + /** + * {@link Node} of the specific origin, Usually the origin is the Service Consumer. + */ + private Node originNode; + private Throwable error; + protected ResourceWrapper resourceWrapper; + + public Entry(ResourceWrapper resourceWrapper) { + this.resourceWrapper = resourceWrapper; + this.createTime = TimeUtil.currentTimeMillis(); + } + + public ResourceWrapper getResourceWrapper() { + return resourceWrapper; + } + + public void exit() throws ErrorEntryFreeException { + exit(1, OBJECTS0); + } + + public void exit(int count) throws ErrorEntryFreeException { + exit(count, OBJECTS0); + } + + /** + * Exit this entry. This method should invoke if and only if once at the end of the resource protection. + * + * @param count tokens to release. + * @param args + * @throws ErrorEntryFreeException, if {@link Context#getCurEntry()} is not this entry. + */ + public abstract void exit(int count, Object... args) throws ErrorEntryFreeException; + + /** + * Exit this entry. + * + * @param count tokens to release. + * @param args + * @return next available entry after exit, that is the parent entry. + * @throws ErrorEntryFreeException, if {@link Context#getCurEntry()} is not this entry. + */ + protected abstract Entry trueExit(int count, Object... args) throws ErrorEntryFreeException; + + /** + * Get related {@link Node} of the parent {@link Entry}. + * + * @return + */ + public abstract Node getLastNode(); + + public long getCreateTime() { + return createTime; + } + + public Node getCurNode() { + return curNode; + } + + public void setCurNode(Node node) { + this.curNode = node; + } + + public Throwable getError() { + return error; + } + + public void setError(Throwable error) { + this.error = error; + } + + /** + * Get origin {@link Node} of the this {@link Entry}. + * + * @return origin {@link Node} of the this {@link Entry}, may be null if no origin specified by + * {@link ContextUtil#enter(String name, String origin)}. + */ + public Node getOriginNode() { + return originNode; + } + + public void setOriginNode(Node originNode) { + this.originNode = originNode; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/EntryType.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/EntryType.java new file mode 100755 index 00000000..311a8a4d --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/EntryType.java @@ -0,0 +1,47 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +/** + * An enum marks resource invocation direction. + * + * @author jialiang.linjl + */ +public enum EntryType { + /** + * Inbound traffic + */ + IN("IN"), + /** + * Outbound traffic + */ + OUT("OUT"); + + private final String name; + + EntryType(String s) { + name = s; + } + + public boolean equalsName(String otherName) { + return name.equals(otherName); + } + + @Override + public String toString() { + return name; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Env.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Env.java new file mode 100755 index 00000000..4a145a24 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Env.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.node.DefaultNodeBuilder; +import com.alibaba.csp.sentinel.node.NodeBuilder; +import com.alibaba.csp.sentinel.slots.DefaultSlotsChainBuilder; +import com.alibaba.csp.sentinel.slots.SlotsChainBuilder; + +/** + * @author jialiang.linjl + */ +public class Env { + + public static final SlotsChainBuilder slotsChainbuilder = new DefaultSlotsChainBuilder(); + public static final NodeBuilder nodeBuilder = new DefaultNodeBuilder(); + public static final Sph sph = new CtSph(); + + static { + // If init fails, the process will exit. + InitExecutor.doInit(); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/ErrorEntryFreeException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/ErrorEntryFreeException.java new file mode 100755 index 00000000..2506ecd1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/ErrorEntryFreeException.java @@ -0,0 +1,28 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +/** + * Represents order mismatch of resource entry and resource exit (pair mismatch). + * + * @author qinan.qn + */ +public class ErrorEntryFreeException extends RuntimeException { + + public ErrorEntryFreeException(String s) { + super(s); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java new file mode 100755 index 00000000..0d8977f8 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Sph.java @@ -0,0 +1,145 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import java.lang.reflect.Method; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * Interface to get {@link Entry} for resource protection. If any block criteria is met, + * a {@link BlockException} or its subclasses will be thrown. Successfully getting a entry + * indicates permitting the invocation pass. + * + * @author qinan.qn + * @author jialiang.linjl + * @author leyou + */ +public interface Sph { + + /** + * Create a protected resource. + * + * @param name the unique name of the protected resource + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(String name) throws BlockException; + + /** + * Create a protected method. + * + * @param method the protected method + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(Method method) throws BlockException; + + /** + * Create a protected method. + * + * @param method the protected method + * @param count the count that the resource requires + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(Method method, int count) throws BlockException; + + /** + * Create a protected resource. + * + * @param name the unique string for the resource + * @param count the count that the resource requires + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(String name, int count) throws BlockException; + + /** + * Create a protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(Method method, EntryType type) throws BlockException; + + /** + * Create a protected resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(String name, EntryType type) throws BlockException; + + /** + * Create a protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable + * @param count the count that the resource requires + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(Method method, EntryType type, int count) throws BlockException; + + /** + * Create a protected resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable + * @param count the count that the resource requires + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(String name, EntryType type, int count) throws BlockException; + + /** + * Create a protected resource. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable + * @param count the count that the resource requires + * @param args the parameters of the method. It can also be counted by setting + * hot parameter rule + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException; + + /** + * Create a protected resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable + * @param count the count that the resource requires + * @param args the parameters of the method. It can also be counted by setting + * hot parameter rule + * @return entry get. + * @throws BlockException if the block criteria is met + */ + Entry entry(String name, EntryType type, int count, Object... args) throws BlockException; + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphO.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphO.java new file mode 100755 index 00000000..49d70613 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphO.java @@ -0,0 +1,225 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import java.lang.reflect.Method; +import java.util.List; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; + +/** + * Conceptually, physical or logical resource that need protection should be + * surrounded by an entry. The requests to this resource will be blocked if any + * criteria is met, eg. when any {@link Rule}'s threshold is exceeded. Once blocked, + * {@link SphU}#enter() will return false. + * + *

+ * To configure the criteria, we can use XXXRuleManager.loadRules() to add rules. eg. + * {@link FlowRuleManager#loadRules(List)}, {@link DegradeRuleManager#loadRules(List)}, + * {@link SystemRuleManager#loadRules(List)}. + *

+ * + *

+ * Following code is an example. {@code "abc"} represent a unique name for the + * protected resource: + *

+ * + *
+ * public void foo() {
+ *    if (SphO.entry("abc")) {
+ *        try {
+ *            // business logic
+ *        } finally {
+ *            SphO.exit(); // must exit()
+ *        }
+ *    } else {
+ *        // failed to enter the protected resource.
+ *    }
+ * }
+ * 
+ * + * Make sure {@code SphO.entry()} and {@link SphO#exit()} be paired in the same thread, + * otherwise {@link ErrorEntryFreeException} will be thrown. + * + * @author jialiang.linjl + * @author leyou + * @see SphU + */ +public class SphO { + + private static final Object[] OBJECTS0 = new Object[0]; + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name of the protected resource + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(String name) { + return entry(name, EntryType.OUT, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(Method method) { + return entry(method, EntryType.OUT, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param count tokens required + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(Method method, int count) { + return entry(method, EntryType.OUT, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique string for the resource + * @param count tokens required + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(String name, int count) { + return entry(name, EntryType.OUT, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(Method method, EntryType type) { + return entry(method, type, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(String name, EntryType type) { + return entry(name, type, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(Method method, EntryType type, int count) { + return entry(method, type, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(String name, EntryType type, int count) { + return entry(name, type, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @param args extra parameters. + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(String name, EntryType type, int count, Object... args) { + try { + Env.sph.entry(name, type, count, args); + } catch (BlockException e) { + return false; + } catch (Throwable e) { + RecordLog.info("[Sentinel] Fatal error", e); + return true; + } + return true; + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @param args the parameters of the method. + * @return true if no rule's threshold is exceeded, otherwise return false. + */ + public static boolean entry(Method method, EntryType type, int count, Object... args) { + try { + Env.sph.entry(method, type, count, args); + } catch (BlockException e) { + return false; + } catch (Throwable e) { + RecordLog.info("[Sentinel] Fatal error", e); + return true; + } + return true; + } + + public static void exit(int count, Object... args) { + ContextUtil.getContext().getCurEntry().exit(count, args); + } + + public static void exit(int count) { + ContextUtil.getContext().getCurEntry().exit(count, OBJECTS0); + } + + public static void exit() { + exit(1, OBJECTS0); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java new file mode 100755 index 00000000..7a8c97e0 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java @@ -0,0 +1,203 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import java.lang.reflect.Method; +import java.util.List; + +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; + +/** + * Conceptually, physical or logical resource that need protection should be + * surrounded by an entry. The requests to this resource will be blocked if any + * criteria is met, eg. when any {@link Rule}'s threshold is exceeded. Once blocked, + * a {@link BlockException} will be thrown. + * + *

+ * To configure the criteria, we can use XXXRuleManager.loadRules() to add rules, eg. + * {@link FlowRuleManager#loadRules(List)}, {@link DegradeRuleManager#loadRules(List)}, + * {@link SystemRuleManager#loadRules(List)}. + *

+ * + *

+ * Following code is an example, {@code "abc"} represent a unique name for the + * protected resource: + *

+ * + *
+ *  public void foo() {
+ *     Entry entry = null;
+ *     try {
+ *        entry = SphU.entry("abc");
+ *        // resource that need protection
+ *     } catch (BlockException blockException) {
+ *         // when goes there, it is blocked
+ *         // add blocked handle logic here
+ *     } catch (Throwable bizException) {
+ *         // business exception
+ *         Tracer.trace(bizException);
+ *     } finally {
+ *         // ensure finally be executed
+ *         if (entry != null){
+ *             entry.exit();
+ *         }
+ *     }
+ *  }
+ * 
+ * + *

+ * Make sure {@code SphU.entry()} and {@link Entry#exit()} be paired in the same thread, + * otherwise {@link ErrorEntryFreeException} will be thrown. + *

+ * + * @author jialiang.linjl + * @see SphO + */ +public class SphU { + + private static final Object[] OBJECTS0 = new Object[0]; + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name of the protected resource + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(String name) throws BlockException { + return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(Method method) throws BlockException { + return Env.sph.entry(method, EntryType.OUT, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param count tokens required + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(Method method, int count) throws BlockException { + return Env.sph.entry(method, EntryType.OUT, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique string for the resource + * @param count tokens required + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(String name, int count) throws BlockException { + return Env.sph.entry(name, EntryType.OUT, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(Method method, EntryType type) throws BlockException { + return Env.sph.entry(method, type, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(String name, EntryType type) throws BlockException { + return Env.sph.entry(name, type, 1, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(Method method, EntryType type, int count) throws BlockException { + return Env.sph.entry(method, type, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(String name, EntryType type, int count) throws BlockException { + return Env.sph.entry(name, type, count, OBJECTS0); + } + + /** + * Checking all {@link Rule}s about the protected method. + * + * @param method the protected method + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @param args the parameters of the method. + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException { + return Env.sph.entry(method, type, count, args); + } + + /** + * Checking all {@link Rule}s about the resource. + * + * @param name the unique name for the protected resource + * @param type the resource is an inbound or an outbound method. This is used + * to mark whether it can be blocked when the system is unstable, + * only inbound traffic could be blocked by {@link SystemRule} + * @param count tokens required + * @param args extra parameters. + * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded. + */ + public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException { + return Env.sph.entry(name, type, count, args); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Tracer.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Tracer.java new file mode 100755 index 00000000..3971ee10 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/Tracer.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * This class is used to record other exception except block exception. + * + * @author jialiang.linjl + */ +public final class Tracer { + + public static void trace(Throwable e) { + trace(e, 1); + } + + public static void trace(Throwable e, int count) { + if (e instanceof BlockException) { + return; + } + + Context context = ContextUtil.getContext(); + if (context == null) { + return; + } + + DefaultNode curNode = (DefaultNode)context.getCurNode(); + if (curNode == null) { + return; + } + + // clusterNode can be null when Constants.ON is false. + ClusterNode clusterNode = curNode.getClusterNode(); + if (clusterNode == null) { + return; + } + clusterNode.trace(e, count); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/concurrent/NamedThreadFactory.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/concurrent/NamedThreadFactory.java new file mode 100755 index 00000000..552ad861 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/concurrent/NamedThreadFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.concurrent; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Wrapped thread factory for better use. + */ +public class NamedThreadFactory implements ThreadFactory { + + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + + private final String namePrefix; + private final boolean daemon; + + public NamedThreadFactory(String namePrefix, boolean daemon) { + this.daemon = daemon; + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : + Thread.currentThread().getThreadGroup(); + this.namePrefix = namePrefix; + } + + public NamedThreadFactory(String namePrefix) { + this(namePrefix, false); + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, namePrefix + "-thread-" + threadNumber.getAndIncrement(), 0); + t.setDaemon(daemon); + return t; + } +} \ No newline at end of file diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java new file mode 100755 index 00000000..d5017f13 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/config/SentinelConfig.java @@ -0,0 +1,149 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.config; + +import java.io.File; +import java.io.FileInputStream; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.log.LogBase; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.AppNameUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * The universal config of Courier. The config is retrieved from + * {@code ${user.home}/logs/csp/${appName}.properties} by default. + * + * @author leyou + */ +public class SentinelConfig { + + private static final Map props = new ConcurrentHashMap(); + + public static final String CHARSET = "csp.sentinel.charset"; + public static final String SINGLE_METRIC_FILE_SIZE = "csp.sentinel.metric.file.single.size"; + public static final String TOTAL_METRIC_FILE_COUNT = "csp.sentinel.metric.file.total.count"; + public static final String COLD_FACTOR = "csp.sentinel.flow.cold.factor"; + + static final long DEFAULT_SINGLE_METRIC_FILE_SIZE = 1024 * 1024 * 50; + static final int DEFAULT_TOTAL_METRIC_FILE_COUNT = 6; + + static { + initialize(); + loadProps(); + } + + private static void initialize() { + // Init default properties. + SentinelConfig.setConfig(CHARSET, "UTF-8"); + SentinelConfig.setConfig(SINGLE_METRIC_FILE_SIZE, String.valueOf(DEFAULT_SINGLE_METRIC_FILE_SIZE)); + SentinelConfig.setConfig(TOTAL_METRIC_FILE_COUNT, String.valueOf(DEFAULT_TOTAL_METRIC_FILE_COUNT)); + SentinelConfig.setConfig(COLD_FACTOR, String.valueOf(3)); + } + + private static void loadProps() { + // Resolve app name. + AppNameUtil.resolveAppName(); + try { + String appName = AppNameUtil.getAppName(); + if (appName == null) { + appName = ""; + } + // We first retrieve the properties from the property file. + String fileName = LogBase.getLogBaseDir() + appName + ".properties"; + File file = new File(fileName); + if (file.exists()) { + RecordLog.info("read SentinelConfig from " + fileName); + FileInputStream fis = new FileInputStream(fileName); + Properties fileProps = new Properties(); + fileProps.load(fis); + fis.close(); + + for (Object key : fileProps.keySet()) { + SentinelConfig.setConfig((String)key, (String)fileProps.get(key)); + try { + String systemValue = System.getProperty((String)key); + if (!StringUtil.isEmpty(systemValue)) { + SentinelConfig.setConfig((String)key, systemValue); + } + } catch (Exception e) { + RecordLog.info(e.getMessage(), e); + } + RecordLog.info(key + " value: " + SentinelConfig.getConfig((String)key)); + } + } + } catch (Throwable ioe) { + RecordLog.info(ioe.getMessage(), ioe); + } + + // JVM parameter override file config. + for (Map.Entry entry : System.getProperties().entrySet()) { + SentinelConfig.setConfig(entry.getKey().toString(), entry.getValue().toString()); + } + } + + /** + * Get config value of the specific key. + * + * @param key config key + * @return the config value. + */ + public static String getConfig(String key) { + return props.get(key); + } + + public static void setConfig(String key, String value) { + props.put(key, value); + } + + public static void setConfigIfAbsent(String key, String value) { + String v = props.get(key); + if (v == null) { + props.put(key, value); + } + } + + public static String getAppName() { + return AppNameUtil.getAppName(); + } + + public static String charset() { + return props.get(CHARSET); + } + + public static long singleMetricFileSize() { + try { + return Long.parseLong(props.get(SINGLE_METRIC_FILE_SIZE)); + } catch (Throwable throwable) { + RecordLog.info("SentinelConfig get singleMetricFileSize fail, use default value: " + + DEFAULT_SINGLE_METRIC_FILE_SIZE, throwable); + return DEFAULT_SINGLE_METRIC_FILE_SIZE; + } + } + + public static int totalMetricFileCount() { + try { + return Integer.parseInt(props.get(TOTAL_METRIC_FILE_COUNT)); + } catch (Throwable throwable) { + RecordLog.info("SentinelConfig get totalMetricFileCount fail, use default value: " + + DEFAULT_TOTAL_METRIC_FILE_COUNT, throwable); + return DEFAULT_TOTAL_METRIC_FILE_COUNT; + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/Context.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/Context.java new file mode 100755 index 00000000..165c98a3 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/Context.java @@ -0,0 +1,164 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.context; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphO; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; + +/** + * This class holds metadata of current invocation:
+ * + *
    + *
  • the {@link EntranceNode}: the root of the current invocation + * tree.
  • + *
  • the current {@link Entry}: the current invocation point.
  • + *
  • the current {@link Node}: the statistics related to the + * {@link Entry}.
  • + *
  • the origin:The origin is useful when we want to control different + * invoker/consumer separately. Usually the origin could be the Service Consumer's app name.
  • + *
+ *

+ * Each {@link SphU}#entry() or {@link SphO}#entry() should be in a {@link Context}, + * if we don't invoke {@link ContextUtil}#enter() explicitly, DEFAULT context will be used. + *

+ *

+ * A invocation tree will be created if we invoke {@link SphU}#entry() multi times in + * the same context. + *

+ *

+ * Same resource in different context will count separately, see {@link NodeSelectorSlot}. + *

+ * + * @author jialiang.linjl + * @author leyou(lihao) + * @author Eric Zhao + * @see ContextUtil + * @see NodeSelectorSlot + */ +public class Context { + + /** + * Context name. + */ + private String name; + + /** + * The entrance node of current invocation tree. + */ + private DefaultNode entranceNode; + + /** + * Current processing entry. + */ + private Entry curEntry; + + /** + * the origin of this context, usually the origin is the Service Consumer's app name. + */ + private String origin = ""; + + public Context(DefaultNode entranceNode, String name) { + super(); + this.name = name; + this.entranceNode = entranceNode; + } + + public String getName() { + return name; + } + + public Node getCurNode() { + return curEntry.getCurNode(); + } + + public void setCurNode(Node node) { + this.curEntry.setCurNode(node); + } + + public Entry getCurEntry() { + return curEntry; + } + + public void setCurEntry(Entry curEntry) { + this.curEntry = curEntry; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public double getOriginTotalQps() { + return getOriginNode() == null ? 0 : getOriginNode().totalQps(); + } + + public double getOriginBlockedQps() { + return getOriginNode() == null ? 0 : getOriginNode().blockedQps(); + } + + public double getOriginPassedReqQps() { + return getOriginNode() == null ? 0 : getOriginNode().successQps(); + } + + public double getOriginPassedQps() { + return getOriginNode() == null ? 0 : getOriginNode().passQps(); + } + + public long getOriginTotalRequest() { + return getOriginNode() == null ? 0 : getOriginNode().totalRequest(); + } + + public long getOriginBlockedRequest() { + return getOriginNode() == null ? 0 : getOriginNode().blockedRequest(); + } + + public double getOriginAvgRt() { + return getOriginNode() == null ? 0 : getOriginNode().avgRt(); + } + + public int getOriginCurThreadNum() { + return getOriginNode() == null ? 0 : getOriginNode().curThreadNum(); + } + + public DefaultNode getEntranceNode() { + return entranceNode; + } + + /** + * Get the parent {@link Node} of the current. + * + * @return the parent node of the current. + */ + public Node getLastNode() { + if (curEntry != null && curEntry.getLastNode() != null) { + return curEntry.getLastNode(); + } else { + return entranceNode; + } + } + + public Node getOriginNode() { + return curEntry == null ? null : curEntry.getOriginNode(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextNameDefineException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextNameDefineException.java new file mode 100755 index 00000000..4798eaa7 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextNameDefineException.java @@ -0,0 +1,26 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.context; + +/** + * @author qinan.qn + */ +public class ContextNameDefineException extends RuntimeException { + + public ContextNameDefineException(String message) { + super(message); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java new file mode 100755 index 00000000..3b582262 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java @@ -0,0 +1,178 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.context; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphO; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; + +/** + * Utility class to get or create {@link Context} in current thread. + * + *

+ * Each {@link SphU}#entry() or {@link SphO}#entry() should be in a {@link Context}. + * If we don't invoke {@link ContextUtil}#enter() explicitly, DEFAULT context will be used. + *

+ * + * @author jialiang.linjl + * @author leyou(lihao) + * @author Eric Zhao + */ +public class ContextUtil { + + /** + * Store the context in ThreadLocal for easy access. + */ + private static ThreadLocal contextHolder = new ThreadLocal(); + + /** + * Holds all {@link EntranceNode} + */ + private static volatile Map contextNameNodeMap = new HashMap(); + + private static final ReentrantLock LOCK = new ReentrantLock(); + private static final Context NULL_CONTEXT = new NullContext(); + + /** + *

+ * Enter the invocation context. The context is ThreadLocal, meaning that + * each thread has it's own {@link Context}. New context will be created if + * current thread doesn't have one. + *

+ *

+ * A context will be related to a {@link EntranceNode}, which represents the entrance + * of the invocation tree. New {@link EntranceNode} will be created if + * current context does't have one. Note that same context name will share + * same {@link EntranceNode} globally. + *

+ *

+ * Note that each distinct {@code origin} of {@code name} will lead to creating a new + * {@link Node}, meaning that total {@link Node} created will be of:
+ * {@code distinct context name count * distinct origin count}
+ * So when origin is too many, memory efficiency should be carefully considered. + *

+ *

+ * Same resource in different context will count separately, see {@link NodeSelectorSlot}. + *

+ * + * @param name the context name. + * @param origin the origin of this invocation, usually the origin could be the Service + * Consumer's app name. The origin is useful when we want to control different + * invoker/consumer separately. + * @return The invocation context of the current thread. + */ + static public Context enter(String name, String origin) { + if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) { + throw new ContextNameDefineException( + "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!"); + } + return trueEnter(name, origin); + } + + protected static Context trueEnter(String name, String origin) { + Context context = contextHolder.get(); + if (context == null) { + Map localCacheNameMap = contextNameNodeMap; + DefaultNode node = localCacheNameMap.get(name); + if (node == null) { + if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { + return NULL_CONTEXT; + } else { + try { + LOCK.lock(); + node = contextNameNodeMap.get(name); + if (node == null) { + if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { + return NULL_CONTEXT; + } else { + node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); + // Add entrance node. + Constants.ROOT.addChild(node); + + Map newMap = new HashMap( + contextNameNodeMap.size() + 1); + newMap.putAll(contextNameNodeMap); + newMap.put(name, node); + contextNameNodeMap = newMap; + } + } + } finally { + LOCK.unlock(); + } + } + } + context = new Context(node, name); + context.setOrigin(origin); + contextHolder.set(context); + } + + return context; + } + + /** + *

+ * Enter the invocation context. The context is ThreadLocal, meaning that + * each thread has it's own {@link Context}. New context will be created if + * current thread doesn't have one. + *

+ *

+ * A context will related to A {@link EntranceNode}, which is the entrance + * of the invocation tree. New {@link EntranceNode} will be created if + * current context does't have one. Note that same resource name will share + * same {@link EntranceNode} globally. + *

+ *

+ * Same resource in different context will count separately, see {@link NodeSelectorSlot}. + *

+ * + * @param name the context name. + * @return The invocation context of the current thread. + */ + public static Context enter(String name) { + return enter(name, ""); + } + + /** + * Exit context of current thread, that is removing {@link Context} in the + * ThreadLocal. + */ + public static void exit() { + Context context = contextHolder.get(); + if (context != null && context.getCurEntry() == null) { + contextHolder.set(null); + } + } + + /** + * Get {@link Context} of current thread. + * + * @return context of current thread. Null value will be return if current + * thread does't have context. + */ + public static Context getContext() { + return contextHolder.get(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/NullContext.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/NullContext.java new file mode 100755 index 00000000..9c68be20 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/NullContext.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.context; + +import com.alibaba.csp.sentinel.Constants; + +/** + * If total {@link Context} exceed {@link Constants#MAX_CONTEXT_NAME_SIZE}, a + * {@link NullContext} will get when invoke {@link ContextUtil}.enter(), means + * no rules checking will do. + * + * @author qinan.qn + */ +public class NullContext extends Context { + + public NullContext() { + super(null, null); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/BaseLoggerBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/BaseLoggerBuilder.java new file mode 100755 index 00000000..870bd92b --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/BaseLoggerBuilder.java @@ -0,0 +1,83 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +class BaseLoggerBuilder> { + + protected final String loggerName; + + protected String filePath = null; + + protected long maxFileSize = 1024; + + protected char entryDelimiter = '|'; + + protected int maxBackupIndex = 3; + + BaseLoggerBuilder(String loggerName) { + this.loggerName = loggerName; + } + + public T logFilePath(String logFilePath) { + return configLogFilePath(logFilePath, EagleEye.EAGLEEYE_LOG_DIR); + } + + public T appFilePath(String appFilePath) { + return configLogFilePath(appFilePath, EagleEye.APP_LOG_DIR); + } + + public T baseLogFilePath(String baseLogFilePath) { + return configLogFilePath(baseLogFilePath, EagleEye.BASE_LOG_DIR); + } + + @SuppressWarnings("unchecked") + private T configLogFilePath(String filePathToConfig, String basePath) { + EagleEyeCoreUtils.checkNotNullEmpty(filePathToConfig, "filePath"); + if (filePathToConfig.charAt(0) != '/') { + filePathToConfig = basePath + filePathToConfig; + } + this.filePath = filePathToConfig; + return (T)this; + } + + @SuppressWarnings("unchecked") + public T maxFileSizeMB(long maxFileSizeMB) { + if (maxFileSize < 10) { + throw new IllegalArgumentException("Invalid maxFileSizeMB"); + } + this.maxFileSize = maxFileSizeMB * 1024 * 1024; + return (T)this; + } + + @SuppressWarnings("unchecked") + public T maxBackupIndex(int maxBackupIndex) { + if (maxBackupIndex < 1) { + throw new IllegalArgumentException(""); + } + this.maxBackupIndex = maxBackupIndex; + return (T)this; + } + + @SuppressWarnings("unchecked") + public T entryDelimiter(char entryDelimiter) { + this.entryDelimiter = entryDelimiter; + return (T)this; + } + + String getLoggerName() { + return loggerName; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEye.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEye.java new file mode 100755 index 00000000..ce593fca --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEye.java @@ -0,0 +1,235 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + +public final class EagleEye { + + public static final String CLASS_LOCATION = getEagleEyeLocation(); + + static final String USER_HOME = locateUserHome(); + + static final String BASE_LOG_DIR = locateBaseLogPath(); + + static final String EAGLEEYE_LOG_DIR = locateEagleEyeLogPath(); + + static final String APP_LOG_DIR = locateAppLogPath(); + + static final Charset DEFAULT_CHARSET = getDefaultOutputCharset(); + + static final String EAGLEEYE_SELF_LOG_FILE = EagleEye.EAGLEEYE_LOG_DIR + "eagleeye-self.log"; + + // 200MB + static final long MAX_SELF_LOG_FILE_SIZE = 200 * 1024 * 1024; + + static EagleEyeAppender selfAppender = createSelfLogger(); + + static private TokenBucket exceptionBucket = new TokenBucket(10, TimeUnit.SECONDS.toMillis(10)); + + static String getEagleEyeLocation() { + try { + URL resource = EagleEye.class.getProtectionDomain().getCodeSource().getLocation(); + if (resource != null) { + return resource.toString(); + } + } catch (Throwable t) { + // ignore + } + return "unknown location"; + } + + static Charset getDefaultOutputCharset() { + Charset cs; + String charsetName = EagleEyeCoreUtils.getSystemProperty("EAGLEEYE.CHARSET"); + if (EagleEyeCoreUtils.isNotBlank(charsetName)) { + charsetName = charsetName.trim(); + try { + cs = Charset.forName(charsetName); + if (cs != null) { + return cs; + } + } catch (Exception e) { + // quietly + } + } + try { + cs = Charset.forName("GB18030"); + } catch (Exception e) { + try { + cs = Charset.forName("GBK"); + } catch (Exception e2) { + cs = Charset.forName("UTF-8"); + } + } + return cs; + } + + private static String locateUserHome() { + String userHome = EagleEyeCoreUtils.getSystemProperty("user.home"); + if (EagleEyeCoreUtils.isNotBlank(userHome)) { + if (!userHome.endsWith(File.separator)) { + userHome += File.separator; + } + } else { + userHome = "/tmp/"; + } + return userHome; + } + + private static String locateBaseLogPath() { + String tmpPath = EagleEyeCoreUtils.getSystemProperty("JM.LOG.PATH"); + if (EagleEyeCoreUtils.isNotBlank(tmpPath)) { + if (!tmpPath.endsWith(File.separator)) { + tmpPath += File.separator; + } + } else { + tmpPath = USER_HOME + "logs" + File.separator; + } + return tmpPath; + } + + private static String locateEagleEyeLogPath() { + String tmpPath = EagleEyeCoreUtils.getSystemProperty("EAGLEEYE.LOG.PATH"); + if (EagleEyeCoreUtils.isNotBlank(tmpPath)) { + if (!tmpPath.endsWith(File.separator)) { + tmpPath += File.separator; + } + } else { + tmpPath = BASE_LOG_DIR + "eagleeye" + File.separator; + } + return tmpPath; + } + + private static String locateAppLogPath() { + String appName = EagleEyeCoreUtils.getSystemProperty("project.name"); + if (EagleEyeCoreUtils.isNotBlank(appName)) { + return USER_HOME + appName + File.separator + "logs" + File.separator; + } else { + return EAGLEEYE_LOG_DIR; + } + } + + static private final EagleEyeAppender createSelfLogger() { + EagleEyeRollingFileAppender selfAppender = new EagleEyeRollingFileAppender(EAGLEEYE_SELF_LOG_FILE, + EagleEyeCoreUtils.getSystemPropertyForLong("EAGLEEYE.LOG.SELF.FILESIZE", MAX_SELF_LOG_FILE_SIZE), + false); + return new SyncAppender(selfAppender); + } + + static { + initEagleEye(); + } + + private static void initEagleEye() { + try { + selfLog("[INFO] EagleEye started (" + CLASS_LOCATION + ")" + ", classloader=" + + EagleEye.class.getClassLoader()); + } catch (Throwable e) { + selfLog("[INFO] EagleEye started (" + CLASS_LOCATION + ")"); + } + + try { + EagleEyeLogDaemon.start(); + } catch (Throwable e) { + selfLog("[ERROR] fail to start EagleEyeLogDaemon", e); + } + try { + StatLogController.start(); + } catch (Throwable e) { + selfLog("[ERROR] fail to start StatLogController", e); + } + + } + + public static void shutdown() { + selfLog("[WARN] EagleEye is shutting down (" + CLASS_LOCATION + ")"); + + EagleEye.flush(); + + try { + StatLogController.stop(); + EagleEye.selfLog("[INFO] StatLogController stopped"); + } catch (Throwable e) { + selfLog("[ERROR] fail to stop StatLogController", e); + } + + try { + EagleEyeLogDaemon.stop(); + EagleEye.selfLog("[INFO] EagleEyeLogDaemon stopped"); + } catch (Throwable e) { + selfLog("[ERROR] fail to stop EagleEyeLogDaemon", e); + } + + EagleEye.selfLog("[WARN] EagleEye shutdown successfully (" + CLASS_LOCATION + ")"); + try { + selfAppender.close(); + } catch (Throwable e) { + // ignore + } + } + + private EagleEye() { + } + + static public StatLogger statLogger(String loggerName) { + return statLoggerBuilder(loggerName).buildSingleton(); + } + + static public StatLoggerBuilder statLoggerBuilder(String loggerName) { + return new StatLoggerBuilder(loggerName); + } + + static void setEagelEyeSelfAppender(EagleEyeAppender appender) { + selfAppender = appender; + } + + public static void selfLog(String log) { + try { + String timestamp = EagleEyeCoreUtils.formatTime(System.currentTimeMillis()); + String line = "[" + timestamp + "] " + log + EagleEyeCoreUtils.NEWLINE; + selfAppender.append(line); + } catch (Throwable t) { + } + } + + public static void selfLog(String log, Throwable e) { + long now = System.currentTimeMillis(); + if (exceptionBucket.accept(now)) { + try { + String timestamp = EagleEyeCoreUtils.formatTime(now); + StringWriter sw = new StringWriter(4096); + PrintWriter pw = new PrintWriter(sw, false); + pw.append('[').append(timestamp).append("] ").append(log).append(EagleEyeCoreUtils.NEWLINE); + e.printStackTrace(pw); + pw.println(); + pw.flush(); + selfAppender.append(sw.toString()); + } catch (Throwable t) { + } + } + } + + static public void flush() { + EagleEyeLogDaemon.flushAndWait(); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeAppender.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeAppender.java new file mode 100755 index 00000000..9501b4c4 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeAppender.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +public abstract class EagleEyeAppender { + + public abstract void append(String log); + + public void flush() { + // do nothing + } + + public void rollOver() { + // do nothing + } + + public void reload() { + // do nothing + } + + public void close() { + // do nothing + } + + public void cleanup() { + // do nothing + } + + public String getOutputLocation() { + return null; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtils.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtils.java new file mode 100755 index 00000000..8874f721 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeCoreUtils.java @@ -0,0 +1,237 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +final class EagleEyeCoreUtils { + + public static final String EMPTY_STRING = ""; + public static final String NEWLINE = "\r\n"; + + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + public static boolean isBlank(String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if ((!Character.isWhitespace(str.charAt(i)))) { + return false; + } + } + return true; + } + + public static String checkNotNullEmpty(String value, String name) throws IllegalArgumentException { + if (isBlank(value)) { + throw new IllegalArgumentException(name + " is null or empty"); + } + return value; + } + + public static T checkNotNull(T value, String name) throws IllegalArgumentException { + if (value == null) { + throw new IllegalArgumentException(name + " is null"); + } + return value; + } + + public static T defaultIfNull(T value, T defaultValue) { + return (value == null) ? defaultValue : value; + } + + public static boolean isNotBlank(String str) { + return !isBlank(str); + } + + public static boolean isNotEmpty(String str) { + return str != null && str.length() > 0; + } + + public static String trim(String str) { + return str == null ? null : str.trim(); + } + + public static String[] split(String str, char separatorChar) { + return splitWorker(str, separatorChar, false); + } + + private static String[] splitWorker(String str, char separatorChar, boolean preserveAllTokens) { + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return EMPTY_STRING_ARRAY; + } + List list = new ArrayList(); + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + while (i < len) { + if (str.charAt(i) == separatorChar) { + if (match || preserveAllTokens) { + list.add(str.substring(start, i)); + match = false; + lastMatch = true; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + if (match || (preserveAllTokens && lastMatch)) { + list.add(str.substring(start, i)); + } + return list.toArray(new String[list.size()]); + } + + public static StringBuilder appendWithBlankCheck(String str, String defaultValue, StringBuilder appender) { + if (isNotBlank(str)) { + appender.append(str); + } else { + appender.append(defaultValue); + } + return appender; + } + + public static StringBuilder appendWithNullCheck(Object obj, String defaultValue, StringBuilder appender) { + if (obj != null) { + appender.append(obj.toString()); + } else { + appender.append(defaultValue); + } + return appender; + } + + public static StringBuilder appendLog(String str, StringBuilder appender, char delimiter) { + if (str != null) { + int len = str.length(); + appender.ensureCapacity(appender.length() + len); + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + if (c == '\n' || c == '\r' || c == delimiter) { + c = ' '; + } + appender.append(c); + } + } + return appender; + } + + private static final ThreadLocal dateFmt = new ThreadLocal() { + @Override + protected FastDateFormat initialValue() { + return new FastDateFormat(); + } + }; + + public static String formatTime(long timestamp) { + return dateFmt.get().format(timestamp); + } + + public static String getSystemProperty(String key) { + try { + return System.getProperty(key); + } catch (Throwable t) { + return null; + } + } + + public static long getSystemPropertyForLong(String key, long defaultValue) { + try { + return Long.parseLong(System.getProperty(key)); + } catch (Throwable t) { + return defaultValue; + } + } + + public static boolean isHexNumeric(char ch) { + return (ch >= 'a' && ch <= 'f') || (ch >= '0' && ch <= '9'); + } + + public static boolean isNumeric(char ch) { + return ch >= '0' && ch <= '9'; + } + + public static void shutdownThreadPool(ExecutorService pool, long awaitTimeMillis) { + try { + pool.shutdown(); + + boolean done = false; + if (awaitTimeMillis > 0) { + try { + done = pool.awaitTermination(awaitTimeMillis, TimeUnit.MILLISECONDS); + } catch (Exception e) { + } + } + + if (!done) { + pool.shutdownNow(); + } + } catch (Exception e) { + // quietly + } + } + + // Unsafe mechanics + @SuppressWarnings("restriction") + private static final sun.misc.Unsafe UNSAFE = doGetUnsafe(); + + @SuppressWarnings("restriction") + public static sun.misc.Unsafe getUnsafe() { + return UNSAFE; + } + + /** + * Returns a sun.misc.Unsafe. Suitable for use in a 3rd party package. + * Replace with a simple call to Unsafe.getUnsafe when integrating into a + * jdk. + * + * @return a sun.misc.Unsafe + */ + @SuppressWarnings("restriction") + private static sun.misc.Unsafe doGetUnsafe() { + try { + return sun.misc.Unsafe.getUnsafe(); + } catch (Throwable tryReflectionInstead) { + } + try { + return java.security.AccessController + .doPrivileged(new java.security.PrivilegedExceptionAction() { + @Override + public sun.misc.Unsafe run() throws Exception { + Class k = sun.misc.Unsafe.class; + for (java.lang.reflect.Field f : k.getDeclaredFields()) { + f.setAccessible(true); + Object x = f.get(null); + if (k.isInstance(x)) { return k.cast(x); } + } + throw new NoSuchFieldError("the Unsafe"); + } + }); + } catch (Throwable t) { + return null; + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeLogDaemon.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeLogDaemon.java new file mode 100755 index 00000000..9e09e789 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeLogDaemon.java @@ -0,0 +1,140 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +class EagleEyeLogDaemon implements Runnable { + + private static final long LOG_CHECK_INTERVAL = TimeUnit.SECONDS.toMillis(20); + + private static AtomicBoolean running = new AtomicBoolean(false); + + private static Thread worker = null; + + private static final CopyOnWriteArrayList watchedAppenders + = new CopyOnWriteArrayList(); + + static EagleEyeAppender watch(EagleEyeAppender appender) { + watchedAppenders.addIfAbsent(appender); + return appender; + } + + static boolean unwatch(EagleEyeAppender appender) { + return watchedAppenders.remove(appender); + } + + @Override + public void run() { + while (running.get()) { + + cleanupFiles(); + + try { + Thread.sleep(LOG_CHECK_INTERVAL); + } catch (InterruptedException e) { + + } + + flushAndReload(); + } + } + + private void cleanupFiles() { + for (EagleEyeAppender watchedAppender : watchedAppenders) { + try { + watchedAppender.cleanup(); + } catch (Exception e) { + EagleEye.selfLog("[ERROR] fail to cleanup: " + watchedAppender, e); + } + } + try { + EagleEye.selfAppender.cleanup(); + } catch (Exception e) { + // quietly + } + } + + private void flushAndReload() { + for (EagleEyeAppender watchedAppender : watchedAppenders) { + try { + watchedAppender.reload(); + } catch (Exception e) { + EagleEye.selfLog("[ERROR] fail to reload: " + watchedAppender, e); + } + } + try { + EagleEye.selfAppender.reload(); + } catch (Exception e) { + // quietly + } + } + + static void start() { + if (running.compareAndSet(false, true)) { + Thread worker = new Thread(new EagleEyeLogDaemon()); + worker.setDaemon(true); + worker.setName("EagleEye-LogDaemon-Thread"); + worker.start(); + EagleEyeLogDaemon.worker = worker; + } + } + + static void stop() { + if (running.compareAndSet(true, false)) { + + closeAppenders(); + + final Thread worker = EagleEyeLogDaemon.worker; + if (worker != null) { + try { + worker.interrupt(); + } catch (Exception e) { + // ignore + } + try { + worker.join(1000); + } catch (Exception e) { + // ignore + } + } + } + } + + private static void closeAppenders() { + for (EagleEyeAppender watchedAppender : watchedAppenders) { + try { + watchedAppender.close(); + } catch (Exception e) { + EagleEye.selfLog("[ERROR] fail to close: " + watchedAppender, e); + } + } + } + + static void flushAndWait() { + for (EagleEyeAppender watchedAppender : watchedAppenders) { + try { + watchedAppender.flush(); + } catch (Exception e) { + EagleEye.selfLog("[ERROR] fail to flush: " + watchedAppender, e); + } + } + } + + private EagleEyeLogDaemon() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeRollingFileAppender.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeRollingFileAppender.java new file mode 100755 index 00000000..b5a8c4a0 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/EagleEyeRollingFileAppender.java @@ -0,0 +1,332 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileLock; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +class EagleEyeRollingFileAppender extends EagleEyeAppender { + + private static final long LOG_FLUSH_INTERVAL = TimeUnit.SECONDS.toMillis(1); + + private static final int DEFAULT_BUFFER_SIZE = 4 * 1024; // 4KB + + private final int maxBackupIndex = 3; + + private final long maxFileSize; + + private final int bufferSize = DEFAULT_BUFFER_SIZE; + + private final String filePath; + + private final AtomicBoolean isRolling = new AtomicBoolean(false); + + private BufferedOutputStream bos = null; + + private long nextFlushTime = 0L; + + private long lastRollOverTime = 0L; + + private long outputByteSize = 0L; + + private final boolean selfLogEnabled; + + private boolean multiProcessDetected = false; + + private static final String DELETE_FILE_SUFFIX = ".deleted"; + + public EagleEyeRollingFileAppender(String filePath, long maxFileSize) { + this(filePath, maxFileSize, true); + } + + public EagleEyeRollingFileAppender(String filePath, long maxFileSize, boolean selfLogEnabled) { + this.filePath = filePath; + this.maxFileSize = maxFileSize; + this.selfLogEnabled = selfLogEnabled; + setFile(); + } + + private void setFile() { + try { + File logFile = new File(filePath); + if (!logFile.exists()) { + File parentFile = logFile.getParentFile(); + if (!parentFile.exists() && !parentFile.mkdirs()) { + doSelfLog("[ERROR] Fail to mkdirs: " + parentFile.getAbsolutePath()); + return; + } + try { + if (!logFile.createNewFile()) { + doSelfLog("[ERROR] Fail to create file, it exists: " + logFile.getAbsolutePath()); + } + } catch (IOException e) { + doSelfLog( + "[ERROR] Fail to create file: " + logFile.getAbsolutePath() + ", error=" + e.getMessage()); + } + } + if (!logFile.isFile() || !logFile.canWrite()) { + doSelfLog("[ERROR] Invalid file, exists=" + logFile.exists() + ", isFile=" + logFile.isFile() + + ", canWrite=" + logFile.canWrite() + ", path=" + logFile.getAbsolutePath()); + return; + } + FileOutputStream ostream = new FileOutputStream(logFile, true); + // true + // O_APPEND + this.bos = new BufferedOutputStream(ostream, bufferSize); + this.lastRollOverTime = System.currentTimeMillis(); + this.outputByteSize = logFile.length(); + } catch (Throwable e) { + doSelfLog("[ERROR] Fail to create file to write: " + filePath + ", error=" + e.getMessage()); + } + } + + @Override + public void append(String log) { + BufferedOutputStream bos = this.bos; + if (bos != null) { + try { + waitUntilRollFinish(); + + byte[] bytes = log.getBytes(EagleEye.DEFAULT_CHARSET); + int len = bytes.length; + if (len > DEFAULT_BUFFER_SIZE && this.multiProcessDetected) { + len = DEFAULT_BUFFER_SIZE; + bytes[len - 1] = '\n'; + } + bos.write(bytes, 0, len); + outputByteSize += len; + + if (outputByteSize >= maxFileSize) { + rollOver(); + } else { + if (System.currentTimeMillis() >= nextFlushTime) { + flush(); + } + } + } catch (Exception e) { + doSelfLog("[ERROR] fail to write log to file " + filePath + ", error=" + e.getMessage()); + close(); + setFile(); + } + } + } + + @Override + public void flush() { + final BufferedOutputStream bos = this.bos; + if (bos != null) { + try { + bos.flush(); + nextFlushTime = System.currentTimeMillis() + LOG_FLUSH_INTERVAL; + } catch (Exception e) { + doSelfLog("[WARN] Fail to flush OutputStream: " + filePath + ", " + e.getMessage()); + } + } + } + + @Override + public void rollOver() { + final String lockFilePath = filePath + ".lock"; + final File lockFile = new File(lockFilePath); + + RandomAccessFile raf = null; + FileLock fileLock = null; + + if (!isRolling.compareAndSet(false, true)) { + return; + } + + try { + raf = new RandomAccessFile(lockFile, "rw"); + fileLock = raf.getChannel().tryLock(); + + if (fileLock != null) { + File target; + File file; + final int maxBackupIndex = this.maxBackupIndex; + + reload(); + if (outputByteSize >= maxFileSize) { + file = new File(filePath + '.' + maxBackupIndex); + if (file.exists()) { + target = new File(filePath + '.' + maxBackupIndex + DELETE_FILE_SUFFIX); + if (!file.renameTo(target) && !file.delete()) { + doSelfLog("[ERROR] Fail to delete or rename file: " + file.getAbsolutePath() + " to " + + target.getAbsolutePath()); + } + } + + for (int i = maxBackupIndex - 1; i >= 1; i--) { + file = new File(filePath + '.' + i); + if (file.exists()) { + target = new File(filePath + '.' + (i + 1)); + if (!file.renameTo(target) && !file.delete()) { + doSelfLog("[ERROR] Fail to delete or rename file: " + file.getAbsolutePath() + " to " + + target.getAbsolutePath()); + } + } + } + + target = new File(filePath + "." + 1); + + close(); + + file = new File(filePath); + if (file.renameTo(target)) { + doSelfLog("[INFO] File rolled to " + target.getAbsolutePath() + ", " + + TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - lastRollOverTime) + + " minutes since last roll"); + } else { + doSelfLog("[WARN] Fail to rename file: " + file.getAbsolutePath() + " to " + + target.getAbsolutePath()); + } + + setFile(); + } + } + } catch (IOException e) { + doSelfLog("[ERROR] Fail rollover file: " + filePath + ", error=" + e.getMessage()); + } finally { + isRolling.set(false); + + if (fileLock != null) { + try { + fileLock.release(); + } catch (IOException e) { + doSelfLog("[ERROR] Fail to release file lock: " + lockFilePath + ", error=" + e.getMessage()); + } + } + + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + doSelfLog("[WARN] Fail to close file lock: " + lockFilePath + ", error=" + e.getMessage()); + } + } + + if (fileLock != null) { + if (!lockFile.delete() && lockFile.exists()) { + doSelfLog("[WARN] Fail to delete file lock: " + lockFilePath); + } + } + } + } + + @Override + public void close() { + BufferedOutputStream bos = this.bos; + if (bos != null) { + try { + bos.close(); + } catch (IOException e) { + doSelfLog("[WARN] Fail to close OutputStream: " + e.getMessage()); + } + this.bos = null; + } + } + + @Override + public void reload() { + flush(); + File logFile = new File(filePath); + long fileSize = logFile.length(); + boolean fileNotExists = fileSize <= 0 && !logFile.exists(); + + if (this.bos == null || fileSize < outputByteSize || fileNotExists) { + doSelfLog("[INFO] Log file rolled over by outside: " + filePath + ", force reload"); + close(); + setFile(); + } else if (fileSize > outputByteSize) { + this.outputByteSize = fileSize; + if (!this.multiProcessDetected) { + this.multiProcessDetected = true; + if (selfLogEnabled) { + doSelfLog("[WARN] Multi-process file write detected: " + filePath); + } + } + } else { + + } + } + + @Override + public void cleanup() { + try { + File logFile = new File(filePath); + File parentDir = logFile.getParentFile(); + if (parentDir != null && parentDir.isDirectory()) { + final String baseFileName = logFile.getName(); + File[] filesToDelete = parentDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if (name != null && name.startsWith(baseFileName) && name.endsWith(DELETE_FILE_SUFFIX)) { + return true; + } + return false; + } + }); + if (filesToDelete != null && filesToDelete.length > 0) { + for (File f : filesToDelete) { + boolean success = f.delete() || !f.exists(); + if (success) { + doSelfLog("[INFO] Deleted log file: " + f.getAbsolutePath()); + } else if (f.exists()) { + doSelfLog("[ERROR] Fail to delete log file: " + f.getAbsolutePath()); + } + } + } + } + } catch (Exception e) { + doSelfLog("[ERROR] Fail to cleanup log file, error=" + e.getMessage()); + } + } + + void waitUntilRollFinish() { + while (isRolling.get()) { + try { + Thread.sleep(1L); + } catch (Exception e) { + // quietly + } + } + } + + private void doSelfLog(String log) { + if (selfLogEnabled) { + EagleEye.selfLog(log); + } else { + System.out.println("[EagleEye]" + log); + } + } + + @Override + public String getOutputLocation() { + return filePath; + } + + @Override + public String toString() { + return "EagleEyeRollingFileAppender [filePath=" + filePath + "]"; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/FastDateFormat.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/FastDateFormat.java new file mode 100755 index 00000000..30a7cbad --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/FastDateFormat.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +class FastDateFormat { + + private final SimpleDateFormat fmt = createSimpleDateFormat(); + + private char[] buffer = new char[23]; + + private long lastSecond = -1; + private long lastMillis = -1; + + public String format(long timestamp) { + formatToBuffer(timestamp); + return new String(buffer, 0, 23); + } + + public String format(Date date) { + return format(date.getTime()); + } + + public void formatAndAppendTo(long timestamp, StringBuilder appender) { + formatToBuffer(timestamp); + appender.append(buffer, 0, 23); + } + + private void formatToBuffer(long timestamp) { + if (timestamp == lastMillis) { + return; + } + long diff = timestamp - lastSecond; + if (diff >= 0 && diff < 1000) { + int ms = (int)(timestamp % 1000); + buffer[22] = (char)(ms % 10 + '0'); + ms /= 10; + buffer[21] = (char)(ms % 10 + '0'); + buffer[20] = (char)(ms / 10 + '0'); + lastMillis = timestamp; + } else { + String result = fmt.format(new Date(timestamp)); + result.getChars(0, result.length(), buffer, 0); + lastSecond = timestamp / 1000 * 1000; + lastMillis = timestamp; + } + } + + String formatWithoutMs(long timestamp) { + long diff = timestamp - lastSecond; + if (diff < 0 || diff >= 1000) { + String result = fmt.format(new Date(timestamp)); + result.getChars(0, result.length(), buffer, 0); + lastSecond = timestamp / 1000 * 1000; + lastMillis = timestamp; + } + return new String(buffer, 0, 19); + } + + private SimpleDateFormat createSimpleDateFormat() { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + fmt.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); + return fmt; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntry.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntry.java new file mode 100755 index 00000000..6858bc52 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntry.java @@ -0,0 +1,179 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.Arrays; +import java.util.List; + +public final class StatEntry { + + private final StatLogger statLogger; + + private final String[] keys; + private transient int hash; + + public StatEntry(StatLogger statLogger, String key) { + this.statLogger = statLogger; + this.keys = new String[] {key}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2, String key3) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2, key3}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2, String key3, String key4) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2, key3, key4}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2, String key3, String key4, String key5) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2, key3, key4, key5}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2, String key3, String key4, String key5, + String key6) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2, key3, key4, key5, key6}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2, String key3, String key4, String key5, + String key6, String key7) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2, key3, key4, key5, key6, key7}; + } + + public StatEntry(StatLogger statLogger, String key1, String key2, String key3, String key4, String key5, + String key6, String key7, String key8) { + this.statLogger = statLogger; + this.keys = new String[] {key1, key2, key3, key4, key5, key6, key7, key8}; + } + + public StatEntry(StatLogger statLogger, String key1, String... moreKeys) { + String[] keys = new String[1 + moreKeys.length]; + keys[0] = key1; + for (int i = 0; i < moreKeys.length; ++i) { + keys[i + 1] = moreKeys[i]; + } + this.statLogger = statLogger; + this.keys = keys; + } + + public StatEntry(StatLogger statLogger, List keys) { + if (keys == null || keys.isEmpty()) { + throw new IllegalArgumentException("keys empty or null: " + keys); + } + this.statLogger = statLogger; + this.keys = keys.toArray(new String[keys.size()]); + } + + public StatEntry(StatLogger statLogger, String[] keys) { + if (keys == null || keys.length == 0) { + throw new IllegalArgumentException("keys empty or null"); + } + this.statLogger = statLogger; + this.keys = Arrays.copyOf(keys, keys.length); + } + + public String[] getKeys() { + return keys; + } + + void appendTo(StringBuilder appender, char delimiter) { + final int len = keys.length; + if (len > 0) { + appender.append(keys[0]); + for (int i = 1; i < len; ++i) { + appender.append(delimiter).append(keys[i]); + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("StatKeys ["); + appendTo(sb, ','); + sb.append("]"); + return sb.toString(); + } + + @Override + public int hashCode() { + if (hash == 0) { + int result = 1; + result = 31 * result + Arrays.hashCode(keys); + hash = result; + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + StatEntry other = (StatEntry)obj; + if (hash != 0 && other.hash != 0 && hash != other.hash) { + return false; + } + if (!Arrays.equals(keys, other.keys)) { + return false; + } + + return true; + } + + StatEntryFunc getFunc(final StatEntryFuncFactory factory) { + return this.statLogger.getRollingData().getStatEntryFunc(this, factory); + } + + public void count() { + count(1); + } + + public void count(long count) { + getFunc(StatEntryFuncFactory.COUNT_SUM).count(count); + } + + public void countAndSum(long valueToSum) { + countAndSum(1, valueToSum); + } + + public void countAndSum(long count, long valueToSum) { + getFunc(StatEntryFuncFactory.COUNT_SUM).countAndSum(count, valueToSum); + } + + public void minMax(long candidate) { + minMax(candidate, null); + } + + public void minMax(long candidate, String ref) { + getFunc(StatEntryFuncFactory.MIN_MAX).minMax(candidate, ref); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntryFunc.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntryFunc.java new file mode 100755 index 00000000..41b6cf8e --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntryFunc.java @@ -0,0 +1,206 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.concurrent.atomic.AtomicReference; + +import com.alibaba.csp.sentinel.slots.statistic.base.LongAdder; + +interface StatEntryFunc { + + void appendTo(StringBuilder appender, char delimiter); + + int getStatType(); + + Object[] getValues(); + + void count(long count); + + void countAndSum(long count, long value); + + void arrayAdd(long... values); + + void arraySet(long... values); + + void minMax(long candidate, String ref); + + void batchAdd(long... values); + + void strArray(String... values); +} + +enum StatEntryFuncFactory { + COUNT_SUM { + @Override + StatEntryFunc create() { + return new StatEntryFuncCountAndSum(); + } + }, + MIN_MAX { + @Override + StatEntryFunc create() { + return new StatEntryFuncMinMax(); + } + }; + + abstract StatEntryFunc create(); +} + +class StatEntryFuncCountAndSum implements StatEntryFunc { + + private LongAdder count = new LongAdder(); + private LongAdder value = new LongAdder(); + + @Override + public void appendTo(StringBuilder appender, char delimiter) { + appender.append(count.sum()).append(delimiter).append(value.sum()); + } + + @Override + public Object[] getValues() { + return new Object[] {count.sum(), value.sum()}; + } + + @Override + public int getStatType() { + return 1; + } + + @Override + public void count(long count) { + this.count.add(count); + } + + @Override + public void countAndSum(long count, long value) { + this.count.add(count); + this.value.add(value); + } + + @Override + public void arrayAdd(long... values) { + throw new IllegalStateException("arrayAdd() is unavailable if countAndSum() has been called"); + } + + @Override + public void arraySet(long... values) { + throw new IllegalStateException("arraySet() is unavailable if countAndSum() has been called"); + } + + @Override + public void minMax(long candidate, String ref) { + throw new IllegalStateException("minMax() is unavailable if countAndSum() has been called"); + } + + @Override + public void batchAdd(long... values) { + throw new IllegalStateException("batchAdd() is unavailable if countAndSum() has been called"); + } + + @Override + public void strArray(String... values) { + throw new IllegalStateException("strArray() is unavailable if countAndSum() has been called"); + } +} + +class StatEntryFuncMinMax implements StatEntryFunc { + + private AtomicReference max = new AtomicReference(new ValueRef(Long.MIN_VALUE, null)); + private AtomicReference min = new AtomicReference(new ValueRef(Long.MAX_VALUE, null)); + + @Override + public void appendTo(StringBuilder appender, char delimiter) { + ValueRef lmax = max.get(); + ValueRef lmin = min.get(); + + appender.append(lmax.value).append(delimiter); + if (lmax.ref != null) { + appender.append(lmax.ref); + } + appender.append(delimiter); + + appender.append(lmin.value).append(delimiter); + if (lmin.ref != null) { + appender.append(lmin.ref); + } + } + + @Override + public Object[] getValues() { + ValueRef lmax = max.get(); + ValueRef lmin = min.get(); + return new Object[] {lmax.value, lmax.ref, lmin.value, lmin.ref}; + } + + @Override + public int getStatType() { + return 4; + } + + @Override + public void count(long count) { + throw new IllegalStateException("count() is unavailable if minMax() has been called"); + } + + @Override + public void countAndSum(long count, long value) { + throw new IllegalStateException("countAndSum() is unavailable if minMax() has been called"); + } + + @Override + public void arrayAdd(long... values) { + throw new IllegalStateException("arrayAdd() is unavailable if minMax() has been called"); + } + + @Override + public void arraySet(long... values) { + throw new IllegalStateException("arraySet() is unavailable if minMax() has been called"); + } + + @Override + public void batchAdd(long... values) { + throw new IllegalStateException("batchAdd() is unavailable if minMax() has been called"); + } + + @Override + public void minMax(long candidate, String ref) { + ValueRef lmax = max.get(); + if (lmax.value <= candidate) { + final ValueRef cmax = new ValueRef(candidate, ref); + while (!max.compareAndSet(lmax, cmax) && (lmax = max.get()).value <= candidate) { ; } + } + ValueRef lmin = min.get(); + if (lmin.value >= candidate) { + final ValueRef cmin = new ValueRef(candidate, ref); + while (!min.compareAndSet(lmin, cmin) && (lmin = min.get()).value >= candidate) { ; } + } + } + + @Override + public void strArray(String... values) { + throw new IllegalStateException("strArray() is unavailable if minMax() has been called"); + } + + private static final class ValueRef { + final long value; + final String ref; + + ValueRef(long value, String ref) { + this.value = value; + this.ref = ref; + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogController.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogController.java new file mode 100755 index 00000000..d7110902 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogController.java @@ -0,0 +1,190 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; + +class StatLogController { + + private static final Map statLoggers = new ConcurrentHashMap(); + + private static final int STAT_ENTRY_COOL_DOWN_MILLIS = 200; + + private static final ScheduledThreadPoolExecutor rollerThreadPool = + new ScheduledThreadPoolExecutor(1, new NamedThreadFactory( + "EagleEye-StatLogController-roller", true)); + + private static final ScheduledThreadPoolExecutor writerThreadPool = + new ScheduledThreadPoolExecutor(1, new NamedThreadFactory( + "EagleEye-StatLogController-writer", true)); + + private static AtomicBoolean running = new AtomicBoolean(false); + + static StatLogger createLoggerIfNotExists(StatLoggerBuilder builder) { + String loggerName = builder.getLoggerName(); + StatLogger statLogger = statLoggers.get(loggerName); + if (statLogger == null) { + synchronized (StatLogController.class) { + if ((statLogger = statLoggers.get(loggerName)) == null) { + statLogger = builder.create(); + statLoggers.put(loggerName, statLogger); + + writerThreadPool.setMaximumPoolSize(Math.max(1, statLoggers.size())); + + scheduleNextRollingTask(statLogger); + EagleEye.selfLog("[INFO] created statLogger[" + statLogger.getLoggerName() + + "]: " + statLogger.getAppender()); + } + } + } + return statLogger; + } + + static Map getAllStatLoggers() { + return Collections.unmodifiableMap(statLoggers); + } + + private static void scheduleNextRollingTask(StatLogger statLogger) { + if (!running.get()) { + EagleEye.selfLog("[INFO] stopped rolling statLogger[" + statLogger.getLoggerName() + "]"); + return; + } + + StatLogRollingTask rollingTask = new StatLogRollingTask(statLogger); + + long rollingTimeMillis = statLogger.getRollingData().getRollingTimeMillis(); + long delayMillis = rollingTimeMillis - System.currentTimeMillis(); + if (delayMillis > 5) { + rollerThreadPool.schedule(rollingTask, delayMillis, TimeUnit.MILLISECONDS); + } else if (-delayMillis > statLogger.getIntervalMillis()) { + EagleEye.selfLog("[WARN] unusual delay of statLogger[" + statLogger.getLoggerName() + + "], delay=" + (-delayMillis) + "ms, submit now"); + rollerThreadPool.submit(rollingTask); + } else { + rollerThreadPool.submit(rollingTask); + } + } + + static void scheduleWriteTask(StatRollingData statRollingData) { + if (statRollingData != null) { + try { + StatLogWriteTask task = new StatLogWriteTask(statRollingData); + writerThreadPool.schedule(task, STAT_ENTRY_COOL_DOWN_MILLIS, TimeUnit.MILLISECONDS); + } catch (Throwable t) { + EagleEye.selfLog("[ERROR] fail to roll statLogger[" + + statRollingData.getStatLogger().getLoggerName() + "]", t); + } + } + } + + private static class StatLogRollingTask implements Runnable { + + final StatLogger statLogger; + + StatLogRollingTask(StatLogger statLogger) { + this.statLogger = statLogger; + } + + @Override + public void run() { + scheduleWriteTask(statLogger.rolling()); + scheduleNextRollingTask(statLogger); + } + } + + private static class StatLogWriteTask implements Runnable { + + final StatRollingData statRollingData; + + StatLogWriteTask(StatRollingData statRollingData) { + this.statRollingData = statRollingData; + } + + @Override + public void run() { + final StatRollingData data = statRollingData; + final StatLogger logger = data.getStatLogger(); + try { + final FastDateFormat fmt = new FastDateFormat(); + final StringBuilder buffer = new StringBuilder(256); + final String timeStr = fmt.formatWithoutMs(data.getTimeSlot()); + + final EagleEyeAppender appender = logger.getAppender(); + final Set> entrySet = data.getStatEntrySet(); + final char entryDelimiter = logger.getEntryDelimiter(); + final char keyDelimiter = logger.getKeyDelimiter(); + final char valueDelimiter = logger.getValueDelimiter(); + + for (Entry entry : entrySet) { + buffer.delete(0, buffer.length()); + StatEntryFunc func = entry.getValue(); + // time|statType|keys|values + buffer.append(timeStr).append(entryDelimiter); + buffer.append(func.getStatType()).append(entryDelimiter); + entry.getKey().appendTo(buffer, keyDelimiter); + buffer.append(entryDelimiter); + func.appendTo(buffer, valueDelimiter); + buffer.append(EagleEyeCoreUtils.NEWLINE); + appender.append(buffer.toString()); + } + + appender.flush(); + } catch (Throwable t) { + EagleEye.selfLog("[WARN] fail to write statLogger[" + + logger.getLoggerName() + "]", t); + } + } + } + + static void start() { + if (running.compareAndSet(false, true)) { + rollerThreadPool.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + writerThreadPool.setExecuteExistingDelayedTasksAfterShutdownPolicy(true); + } + } + + static void stop() { + if (running.compareAndSet(true, false)) { + EagleEyeCoreUtils.shutdownThreadPool(rollerThreadPool, 0); + EagleEye.selfLog("[INFO] StatLoggerController: roller ThreadPool shutdown successfully"); + + for (StatLogger statLogger : statLoggers.values()) { + new StatLogRollingTask(statLogger).run(); + } + + try { + Thread.sleep(STAT_ENTRY_COOL_DOWN_MILLIS); + } catch (InterruptedException e) { + // quietly + } + + EagleEyeCoreUtils.shutdownThreadPool(writerThreadPool, 2000); + EagleEye.selfLog("[INFO] StatLoggerController: writer ThreadPool shutdown successfully"); + } + } + + private StatLogController() { + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogger.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogger.java new file mode 100755 index 00000000..62c613de --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLogger.java @@ -0,0 +1,145 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author jifeng + */ +public final class StatLogger { + + private final String loggerName; + + private final EagleEyeAppender appender; + + private final AtomicReference ref; + + private final long intervalMillis; + + private final int maxEntryCount; + + private final char entryDelimiter; + private final char keyDelimiter; + private final char valueDelimiter; + + StatLogger(String loggerName, EagleEyeAppender appender, long intervalMillis, int maxEntryCount, + char entryDelimiter, char keyDelimiter, char valueDelimiter) { + this.loggerName = loggerName; + this.appender = appender; + this.intervalMillis = intervalMillis; + this.maxEntryCount = maxEntryCount; + this.entryDelimiter = entryDelimiter; + this.keyDelimiter = keyDelimiter; + this.valueDelimiter = valueDelimiter; + this.ref = new AtomicReference(); + rolling(); + } + + public String getLoggerName() { + return loggerName; + } + + EagleEyeAppender getAppender() { + return appender; + } + + StatRollingData getRollingData() { + return ref.get(); + } + + long getIntervalMillis() { + return intervalMillis; + } + + int getMaxEntryCount() { + return maxEntryCount; + } + + char getEntryDelimiter() { + return entryDelimiter; + } + + char getKeyDelimiter() { + return keyDelimiter; + } + + char getValueDelimiter() { + return valueDelimiter; + } + + StatRollingData rolling() { + do { + long now = System.currentTimeMillis(); + long timeSlot = now - now % intervalMillis; + + StatRollingData prevData = ref.get(); + long rollingTimeMillis = timeSlot + intervalMillis; + int initialCapacity = prevData != null ? prevData.getStatCount() : 16; + StatRollingData nextData = new StatRollingData( + this, initialCapacity, timeSlot, rollingTimeMillis); + if (ref.compareAndSet(prevData, nextData)) { + return prevData; + } + } while (true); + } + + public StatEntry stat(String key) { + return new StatEntry(this, key); + } + + public StatEntry stat(String key1, String key2) { + return new StatEntry(this, key1, key2); + } + + public StatEntry stat(String key1, String key2, String key3) { + return new StatEntry(this, key1, key2, key3); + } + + public StatEntry stat(String key1, String key2, String key3, String key4) { + return new StatEntry(this, key1, key2, key3, key4); + } + + public StatEntry stat(String key1, String key2, String key3, String key4, String key5) { + return new StatEntry(this, key1, key2, key3, key4, key5); + } + + public StatEntry stat(String key1, String key2, String key3, String key4, String key5, String key6) { + return new StatEntry(this, key1, key2, key3, key4, key5, key6); + } + + public StatEntry stat(String key1, String key2, String key3, String key4, String key5, String key6, String key7) { + return new StatEntry(this, key1, key2, key3, key4, key5, key6, key7); + } + + public StatEntry stat(String key1, String key2, String key3, String key4, String key5, String key6, String key7, + String key8) { + return new StatEntry(this, key1, key2, key3, key4, key5, key6, key7, key8); + } + + public StatEntry stat(String key1, String... moreKeys) { + return new StatEntry(this, key1, moreKeys); + } + + public StatEntry stat(List keys) { + return new StatEntry(this, keys); + } + + public StatEntry stat(String[] keys) { + return new StatEntry(this, keys); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLoggerBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLoggerBuilder.java new file mode 100755 index 00000000..dcae2751 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatLoggerBuilder.java @@ -0,0 +1,113 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.concurrent.TimeUnit; + +/** + * @author jifeng + */ +public final class StatLoggerBuilder extends BaseLoggerBuilder { + + private int intervalSeconds = 60; + + private int maxEntryCount = 20000; + + private char keyDelimiter = ','; + + private char valueDelimiter = ','; + + private EagleEyeAppender appender = null; + + StatLoggerBuilder(String loggerName) { + super(loggerName); + } + + public StatLoggerBuilder intervalSeconds(int intervalSeconds) { + validateInterval(intervalSeconds); + this.intervalSeconds = intervalSeconds; + return this; + } + + public StatLoggerBuilder maxEntryCount(int maxEntryCount) { + if (maxEntryCount < 1) { + throw new IllegalArgumentException("Max entry count should be at least 1: " + maxEntryCount); + } + this.maxEntryCount = maxEntryCount; + return this; + } + + public StatLoggerBuilder keyDelimiter(char keyDelimiter) { + this.keyDelimiter = keyDelimiter; + return this; + } + + public StatLoggerBuilder valueDelimiter(char valueDelimiter) { + this.valueDelimiter = valueDelimiter; + return this; + } + + StatLoggerBuilder appender(EagleEyeAppender appender) { + this.appender = appender; + return this; + } + + StatLogger create() { + long intervalMillis = TimeUnit.SECONDS.toMillis(this.intervalSeconds); + + String filePath; + if (this.filePath == null) { + filePath = EagleEye.EAGLEEYE_LOG_DIR + "stat-" + loggerName + ".log"; + } else if (this.filePath.endsWith("/") || this.filePath.endsWith("\\")) { + filePath = this.filePath + "stat-" + loggerName + ".log"; + } else { + filePath = this.filePath; + } + + EagleEyeAppender appender = this.appender; + if (appender == null) { + EagleEyeRollingFileAppender rfAppender = new EagleEyeRollingFileAppender(filePath, maxFileSize); + appender = new SyncAppender(rfAppender); + } + + EagleEyeLogDaemon.watch(appender); + return new StatLogger(loggerName, appender, intervalMillis, maxEntryCount, + entryDelimiter, keyDelimiter, valueDelimiter); + } + + public StatLogger buildSingleton() { + return StatLogController.createLoggerIfNotExists(this); + } + + static void validateInterval(final long intervalSeconds) throws IllegalArgumentException { + if (intervalSeconds < 1) { + throw new IllegalArgumentException("Interval cannot be less than 1" + intervalSeconds); + } else if (intervalSeconds < 60) { + if (60 % intervalSeconds != 0) { + throw new IllegalArgumentException("Invalid second interval (cannot divide by 60): " + intervalSeconds); + } + } else if (intervalSeconds <= 5 * 60) { + if (intervalSeconds % 60 != 0) { + throw new IllegalArgumentException("Invalid second interval (cannot divide by 60): " + intervalSeconds); + } + if (60 % intervalSeconds != 0) { + throw new IllegalArgumentException("Invalid second interval (cannot divide by 60): " + intervalSeconds); + } + } else if (intervalSeconds > 5 * 60) { + throw new IllegalArgumentException("Interval should be less than 5 min: " + intervalSeconds); + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatRollingData.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatRollingData.java new file mode 100755 index 00000000..9b804b31 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatRollingData.java @@ -0,0 +1,108 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author jifeng + */ +final class StatRollingData { + + private final StatLogger statLogger; + + private final long timeSlot; + + private final long rollingTimeMillis; + + private final ReentrantLock writeLock; + + private final Map statMap; + + StatRollingData(StatLogger statLogger, int initialCapacity, long timeSlot, long rollingTimeMillis) { + this(statLogger, timeSlot, rollingTimeMillis, + new ConcurrentHashMap( + Math.min(initialCapacity, statLogger.getMaxEntryCount()))); + } + + private StatRollingData(StatLogger statLogger, long timeSlot, long rollingTimeMillis, + Map statMap) { + this.statLogger = statLogger; + this.timeSlot = timeSlot; + this.rollingTimeMillis = rollingTimeMillis; + this.writeLock = new ReentrantLock(); + this.statMap = statMap; + } + + StatEntryFunc getStatEntryFunc( + final StatEntry statEntry, final StatEntryFuncFactory factory) { + StatEntryFunc func = statMap.get(statEntry); + if (func == null) { + StatRollingData clone = null; + writeLock.lock(); + try { + int entryCount = statMap.size(); + if (entryCount < statLogger.getMaxEntryCount()) { + func = statMap.get(statEntry); + if (func == null) { + func = factory.create(); + statMap.put(statEntry, func); + } + } else { + Map cloneStatMap = + new HashMap(statMap); + statMap.clear(); + + func = factory.create(); + statMap.put(statEntry, func); + clone = new StatRollingData(statLogger, timeSlot, rollingTimeMillis, cloneStatMap); + } + } finally { + writeLock.unlock(); + } + + if (clone != null) { + StatLogController.scheduleWriteTask(clone); + } + } + return func; + } + + StatLogger getStatLogger() { + return statLogger; + } + + long getRollingTimeMillis() { + return rollingTimeMillis; + } + + long getTimeSlot() { + return timeSlot; + } + + int getStatCount() { + return statMap.size(); + } + + Set> getStatEntrySet() { + return statMap.entrySet(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/SyncAppender.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/SyncAppender.java new file mode 100755 index 00000000..ea8592ad --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/SyncAppender.java @@ -0,0 +1,79 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +/** + * @author jifeng + */ +final class SyncAppender extends EagleEyeAppender { + + private final EagleEyeAppender delegate; + private final Object lock = new Object(); + + public SyncAppender(EagleEyeAppender delegate) { + this.delegate = delegate; + } + + @Override + public void append(String log) { + synchronized (lock) { + delegate.append(log); + } + } + + @Override + public void flush() { + synchronized (lock) { + delegate.flush(); + } + } + + @Override + public void rollOver() { + synchronized (lock) { + delegate.rollOver(); + } + } + + @Override + public void reload() { + synchronized (lock) { + delegate.reload(); + } + } + + @Override + public void close() { + synchronized (lock) { + delegate.close(); + } + } + + @Override + public void cleanup() { + delegate.cleanup(); + } + + @Override + public String getOutputLocation() { + return delegate.getOutputLocation(); + } + + @Override + public String toString() { + return "SyncAppender [appender=" + delegate + "]"; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/TokenBucket.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/TokenBucket.java new file mode 100755 index 00000000..c2e616b8 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/TokenBucket.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.eagleeye; + +import java.util.concurrent.atomic.AtomicLong; + +class TokenBucket { + + private final long maxTokens; + + private final long intervalMillis; + + private volatile long nextUpdate; + + private AtomicLong tokens; + + public TokenBucket(long maxTokens, long intervalMillis) { + if (maxTokens <= 0) { + throw new IllegalArgumentException("maxTokens should > 0, but given: " + maxTokens); + } + if (intervalMillis < 1000) { + throw new IllegalArgumentException("intervalMillis should be at least 1000, but given: " + intervalMillis); + } + this.maxTokens = maxTokens; + this.intervalMillis = intervalMillis; + this.nextUpdate = System.currentTimeMillis() / 1000 * 1000 + intervalMillis; + this.tokens = new AtomicLong(maxTokens); + } + + public boolean accept(long now) { + long currTokens; + if (now > nextUpdate) { + currTokens = tokens.get(); + if (tokens.compareAndSet(currTokens, maxTokens)) { + nextUpdate = System.currentTimeMillis() / 1000 * 1000 + intervalMillis; + } + } + + do { + currTokens = tokens.get(); + } while (currTokens > 0 && !tokens.compareAndSet(currTokens, currTokens - 1)); + + return currTokens > 0; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java new file mode 100755 index 00000000..d0628ccf --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitExecutor.java @@ -0,0 +1,101 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.init; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + * Load registered init functions and execute in order. + * + * @author Eric Zhao + */ +public final class InitExecutor { + + private static AtomicBoolean initialized = new AtomicBoolean(false); + + /** + * If one {@link InitFunc} throws an exception, the init process + * will immediately be interrupted and the application will exit. + * + * The initialization will be executed only once. + */ + public static void doInit() { + if (!initialized.compareAndSet(false, true)) { + return; + } + try { + ServiceLoader loader = ServiceLoader.load(InitFunc.class); + List initList = new ArrayList(); + for (InitFunc initFunc : loader) { + RecordLog.info("[Sentinel InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName()); + insertSorted(initList, initFunc); + } + for (OrderWrapper w : initList) { + w.func.init(); + RecordLog.info(String.format("[Sentinel InitExecutor] Initialized: %s with order %d", + w.func.getClass().getCanonicalName(), w.order)); + } + } catch (Exception ex) { + RecordLog.info("[Sentinel InitExecutor] Init failed", ex); + ex.printStackTrace(); + System.exit(-1); + } + } + + private static void insertSorted(List list, InitFunc func) { + int order = resolveOrder(func); + int idx = 0; + for (; idx < list.size(); idx++) { + if (list.get(idx).getOrder() > order) { + break; + } + } + list.add(idx, new OrderWrapper(order, func)); + } + + private static int resolveOrder(InitFunc func) { + if (!func.getClass().isAnnotationPresent(InitOrder.class)) { + return InitOrder.LOWEST_PRECEDENCE; + } else { + return func.getClass().getAnnotation(InitOrder.class).value(); + } + } + + private InitExecutor() {} + + private static class OrderWrapper { + private final int order; + private final InitFunc func; + + OrderWrapper(int order, InitFunc func) { + this.order = order; + this.func = func; + } + + int getOrder() { + return order; + } + + InitFunc getFunc() { + return func; + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitFunc.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitFunc.java new file mode 100755 index 00000000..7df4495d --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitFunc.java @@ -0,0 +1,24 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.init; + +/** + * @author Eric Zhao + */ +public interface InitFunc { + + void init() throws Exception; +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitOrder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitOrder.java new file mode 100755 index 00000000..edd81b4e --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/init/InitOrder.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.init; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Eric Zhao + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Documented +public @interface InitOrder { + + int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + + /** + * The order value. Lowest precedence by default. + * + * @return the order value + */ + int value() default LOWEST_PRECEDENCE; +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CommandCenterLog.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CommandCenterLog.java new file mode 100755 index 00000000..f5f34243 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CommandCenterLog.java @@ -0,0 +1,57 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.log; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Logger for command center. + */ +public class CommandCenterLog extends LogBase { + + private static final Logger heliumRecordLog = Logger.getLogger("cspMetricLog"); + private static final String FILE_NAME = "metricStat.log"; + private static Handler logHandler = null; + + static { + logHandler = makeLogger(FILE_NAME, heliumRecordLog); + } + + /** + * Change log dir, the dir will be created if not exits + */ + public static void resetLogBaseDir(String baseDir) { + setLogBaseDir(baseDir); + logHandler = makeLogger(FILE_NAME, heliumRecordLog); + } + + public static void info(String msg) { + LoggerUtils.disableOtherHandlers(heliumRecordLog, logHandler); + heliumRecordLog.log(Level.INFO, msg); + } + + public static void info(String msg, Throwable e) { + LoggerUtils.disableOtherHandlers(heliumRecordLog, logHandler); + heliumRecordLog.log(Level.INFO, msg, e); + } + + public static void warn(String msg, Throwable e) { + LoggerUtils.disableOtherHandlers(heliumRecordLog, logHandler); + heliumRecordLog.log(Level.WARNING, msg, e); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java new file mode 100755 index 00000000..84925614 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/CspFormatter.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * @author xuyue + */ +class CspFormatter extends Formatter { + + private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + @Override + public String format(LogRecord record) { + StringBuilder builder = new StringBuilder(1000); + builder.append(df.format(new Date(record.getMillis()))).append(" "); + builder.append(formatMessage(record)); + + String throwable = ""; + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + record.getThrown().printStackTrace(pw); + pw.close(); + throwable = sw.toString(); + } + builder.append(throwable); + if ("".equals(throwable)) { + builder.append("\n"); + } + return builder.toString(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/DateFileLogHandler.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/DateFileLogHandler.java new file mode 100755 index 00000000..6d119658 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/DateFileLogHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.log; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +class DateFileLogHandler extends Handler { + + private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + private volatile FileHandler handler; + + private final String pattern; + private final int limit; + private final int count; + private final boolean append; + + private volatile boolean initialized = false; + + private volatile long startDate = System.currentTimeMillis(); + private volatile long endDate; + + private final Object monitor = new Object(); + + DateFileLogHandler(String pattern, int limit, int count, boolean append) throws SecurityException { + this.pattern = pattern; + this.limit = limit; + this.count = count; + this.append = append; + rotateDate(); + this.initialized = true; + } + + @Override + public void close() throws SecurityException { + handler.close(); + } + + @Override + public void flush() { + handler.flush(); + } + + @Override + public void publish(LogRecord record) { + synchronized (monitor) { + if (endDate < record.getMillis() || !logFileExits()) { rotateDate(); } + } + + if (System.currentTimeMillis() - startDate > 25 * 60 * 60 * 1000) { + String msg = record.getMessage(); + record.setMessage("missed file rolling at: " + new Date(endDate) + "\n" + msg); + } + handler.publish(record); + } + + @Override + public void setFormatter(Formatter newFormatter) { + super.setFormatter(newFormatter); + if (handler != null) { handler.setFormatter(newFormatter); } + } + + private boolean logFileExits() { + try { + File logFile = new File(pattern); + return logFile.exists(); + } catch (Throwable e) { + + } + return false; + } + + private void rotateDate() { + this.startDate = System.currentTimeMillis(); + if (handler != null) { handler.close(); } + String newPattern = pattern.replace("%d", format.format(new Date())); + // Get current date. + Calendar next = Calendar.getInstance(); + // Begin of next date. + next.set(Calendar.HOUR_OF_DAY, 0); + next.set(Calendar.MINUTE, 0); + next.set(Calendar.SECOND, 0); + next.set(Calendar.MILLISECOND, 0); + next.add(Calendar.DATE, 1); + this.endDate = next.getTimeInMillis(); + + try { + this.handler = new FileHandler(newPattern, limit, count, append); + if (initialized) { + handler.setEncoding(this.getEncoding()); + handler.setErrorManager(this.getErrorManager()); + handler.setFilter(this.getFilter()); + handler.setFormatter(this.getFormatter()); + handler.setLevel(this.getLevel()); + } + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java new file mode 100755 index 00000000..3d8e58ff --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LogBase.java @@ -0,0 +1,83 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.alibaba.csp.sentinel.util.PidUtil; + +/** + * @author leyou + */ +public class LogBase { + public static final String LOG_CHARSET = "utf-8"; + private static final String DIR_NAME = "logs" + File.separator + "csp"; + private static final String USER_HOME = "user.home"; + private static String logBaseDir; + + static { + String userHome = System.getProperty(USER_HOME); + setLogBaseDir(userHome); + } + + /** + * Get log file base directory path, the returned path is guaranteed end with {@link File#separator} + * + * @return log file base directory path. + */ + public static String getLogBaseDir() { + return logBaseDir; + } + + /** + * Change log dir, the dir will be created if not exits + * + * @param baseDir + */ + protected static void setLogBaseDir(String baseDir) { + if (!baseDir.endsWith(File.separator)) { + baseDir += File.separator; + } + String path = baseDir + DIR_NAME + File.separator; + File dir = new File(path); + if (!dir.exists()) { + dir.mkdirs(); + } + logBaseDir = path; + } + + protected static Handler makeLogger(String logName, Logger heliumRecordLog) { + CspFormatter formatter = new CspFormatter(); + String fileName = LogBase.getLogBaseDir() + logName + ".pid" + PidUtil.getPid(); + Handler handler = null; + try { + handler = new DateFileLogHandler(fileName + ".%d", 1024 * 1024 * 200, 1, true); + handler.setFormatter(formatter); + handler.setEncoding(LOG_CHARSET); + } catch (IOException e) { + e.printStackTrace(); + } + if (handler != null) { + LoggerUtils.disableOtherHandlers(heliumRecordLog, handler); + } + heliumRecordLog.setLevel(Level.ALL); + return handler; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LoggerUtils.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LoggerUtils.java new file mode 100755 index 00000000..2fe0d468 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/LoggerUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.log; + +import java.util.logging.Handler; +import java.util.logging.Logger; + +/** + * Util class for logger. + */ +class LoggerUtils { + + /** + * Remove all current handlers from the logger and attach it with the given log handler. + * + * @param logger logger + * @param handler the log handler + */ + static void disableOtherHandlers(Logger logger, Handler handler) { + if (logger == null) { + return; + } + + synchronized (logger) { + Handler[] handlers = logger.getHandlers(); + if (handlers == null) { + return; + } + if (handlers.length == 1 && handlers[0].equals(handler)) { + return; + } + + logger.setUseParentHandlers(false); + // Remove all current handlers. + for (Handler h : handlers) { + logger.removeHandler(h); + } + // Attach the given handler. + logger.addHandler(handler); + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/RecordLog.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/RecordLog.java new file mode 100755 index 00000000..a4f8b7b1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/log/RecordLog.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.log; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/*** + * The basic logger for vital events. + * + * @author youji.zj + */ +public class RecordLog extends LogBase { + private static final Logger heliumRecordLog = Logger.getLogger("cspRecordLog"); + private static final String FILE_NAME = "record.log"; + private static Handler logHandler = null; + + static { + logHandler = makeLogger(FILE_NAME, heliumRecordLog); + } + + /** + * Change log dir, the dir will be created if not exits + */ + public static void resetLogBaseDir(String baseDir) { + setLogBaseDir(baseDir); + logHandler = makeLogger(FILE_NAME, heliumRecordLog); + } + + public static void info(String detail) { + LoggerUtils.disableOtherHandlers(heliumRecordLog, logHandler); + heliumRecordLog.log(Level.INFO, detail); + } + + public static void info(String detail, Throwable e) { + LoggerUtils.disableOtherHandlers(heliumRecordLog, logHandler); + heliumRecordLog.log(Level.INFO, detail, e); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java new file mode 100755 index 00000000..45252de3 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/ClusterNode.java @@ -0,0 +1,100 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import java.util.HashMap; +import java.util.concurrent.locks.ReentrantLock; + +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + *

+ * This class stores summary runtime statistics of the resource, including rt, thread count, qps + * and so on. Same resource shares the same {@link ClusterNode} globally, no matter in witch + * {@link com.alibaba.csp.sentinel.context.Context}. + *

+ *

+ * To distinguish invocation from different origin (declared in + * {@link ContextUtil#enter(String name, String origin)}), + * one {@link ClusterNode} holds an {@link #originCountMap}, this map holds {@link StatisticNode} + * of different origin. Use {@link #getOriginNode(String)} to get {@link Node} of the specific + * origin.
+ * Note that 'origin' usually is Service Consumer's app name. + *

+ * + * @author qinan.qn + * @author jialiang.linjl + */ +public class ClusterNode extends StatisticNode { + + /** + *

+ * the longer the application runs, the more stable this mapping will + * become. so we don't concurrent map but a lock. as this lock only happens + * at the very beginning while concurrent map will hold the lock all the + * time + *

+ */ + private HashMap originCountMap = new HashMap(); + private ReentrantLock lock = new ReentrantLock(); + + /** + * Get {@link Node} of the specific origin. Usually the origin is the Service Consumer's app name. + * + * @param origin The caller's name. It is declared in the + * {@link ContextUtil#enter(String name, String origin)}. + * @return the {@link Node} of the specific origin. + */ + public Node getOriginNode(String origin) { + StatisticNode statisticNode = originCountMap.get(origin); + if (statisticNode == null) { + try { + lock.lock(); + statisticNode = originCountMap.get(origin); + if (statisticNode == null) { + statisticNode = new StatisticNode(); + HashMap newMap = new HashMap( + originCountMap.size() + 1); + newMap.putAll(originCountMap); + newMap.put(origin, statisticNode); + originCountMap = newMap; + } + } finally { + lock.unlock(); + } + } + return statisticNode; + } + + public synchronized HashMap getOriginCountMap() { + return originCountMap; + } + + /** + * Add exception count only when {@code throwable} is not {@link BlockException#isBlockException(Throwable)} + * + * @param throwable + * @param count count to add. + */ + public void trace(Throwable throwable, int count) { + if (!BlockException.isBlockException(throwable)) { + for (int i = 0; i < count; i++) { + this.increaseExceptionQps(); + } + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNode.java new file mode 100755 index 00000000..46383580 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNode.java @@ -0,0 +1,151 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import java.util.HashSet; +import java.util.Set; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.SphO; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; + +/** + *

+ * A {@link Node} use to hold statistics for specific resource name in the specific context. + * Each distinct resource in each distinct {@link Context} will corresponding to a {@link DefaultNode}. + *

+ *

+ * This class may have a list of sub {@link DefaultNode}s. sub-node will be created when + * call {@link SphU}#entry() or {@link SphO}@entry() multi times in the same {@link Context}. + *

+ * + * @author qinan.qn + * @see NodeSelectorSlot + */ +public class DefaultNode extends StatisticNode { + + private ResourceWrapper id; + + private volatile HashSet childList = new HashSet(); + + private ClusterNode clusterNode; + + public DefaultNode(ResourceWrapper id, ClusterNode clusterNode) { + this.id = id; + this.clusterNode = clusterNode; + } + + public ResourceWrapper getId() { + return id; + } + + public ClusterNode getClusterNode() { + return clusterNode; + } + + public void setClusterNode(ClusterNode clusterNode) { + this.clusterNode = clusterNode; + } + + public void addChild(Node node) { + + if (!childList.contains(node)) { + + synchronized (this) { + if (!childList.contains(node)) { + HashSet newSet = new HashSet(childList.size() + 1); + newSet.addAll(childList); + newSet.add(node); + childList = newSet; + } + } + RecordLog.info(String.format("Add child %s to %s", ((DefaultNode)node).id.getName(), id.getName())); + } + } + + public void removeChildList() { + this.childList = new HashSet(); + } + + public Set getChildList() { + return childList; + } + + @Override + public void increaseBlockedQps() { + super.increaseBlockedQps(); + this.clusterNode.increaseBlockedQps(); + } + + @Override + public void increaseExceptionQps() { + super.increaseExceptionQps(); + this.clusterNode.increaseExceptionQps(); + } + + @Override + public void rt(long rt) { + super.rt(rt); + this.clusterNode.rt(rt); + } + + @Override + public void increaseThreadNum() { + super.increaseThreadNum(); + this.clusterNode.increaseThreadNum(); + } + + @Override + public void decreaseThreadNum() { + super.decreaseThreadNum(); + this.clusterNode.decreaseThreadNum(); + } + + @Override + public void addPassRequest() { + super.addPassRequest(); + this.clusterNode.addPassRequest(); + } + + public void printDefaultNode() { + visitTree(0, this); + } + + private void visitTree(int level, DefaultNode node) { + for (int i = 0; i < level; ++i) { + System.out.print("-"); + } + if (!(node instanceof EntranceNode)) { + System.out.println( + String.format("%s(thread:%s pq:%s bq:%s tq:%s rt:%s 1mp:%s 1mb:%s 1mt:%s)", node.id.getShowName(), + node.curThreadNum(), node.passQps(), node.blockedQps(), node.totalQps(), node.avgRt(), + node.totalRequest() - node.blockedRequest(), node.blockedRequest(), node.totalRequest())); + } else { + System.out.println( + String.format("Entry-%s(t:%s pq:%s bq:%s tq:%s rt:%s 1mp:%s 1mb:%s 1mt:%s)", node.id.getShowName(), + node.curThreadNum(), node.passQps(), node.blockedQps(), node.totalQps(), node.avgRt(), + node.totalRequest() - node.blockedRequest(), node.blockedRequest(), node.totalRequest())); + } + for (Node n : node.getChildList()) { + DefaultNode dn = (DefaultNode)n; + visitTree(level + 1, dn); + } + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNodeBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNodeBuilder.java new file mode 100755 index 00000000..a74dde3b --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/DefaultNodeBuilder.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * @author qinan.qn + */ +public class DefaultNodeBuilder implements NodeBuilder { + + @Override + public DefaultNode buildTreeNode(ResourceWrapper id, ClusterNode clusterNode) { + return new DefaultNode(id, clusterNode); + } + + @Override + public ClusterNode buildClusterNode() { + return new ClusterNode(); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/EntranceNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/EntranceNode.java new file mode 100755 index 00000000..cf94b197 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/EntranceNode.java @@ -0,0 +1,119 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; + +/** + *

+ * A {@link Node} represents the entrance of the invocation tree. + *

+ *

+ * One {@link Context} will related to a {@link EntranceNode}, + * which represents the entrance of the invocation tree. New {@link EntranceNode} will be created if + * current context does't have one. Note that same context name will share same {@link EntranceNode} + * globally. + *

+ * + * @author qinan.qn + * @see ContextUtil + * @see ContextUtil#enter(String, String) + * @see NodeSelectorSlot + */ +public class EntranceNode extends DefaultNode { + + public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) { + super(id, clusterNode); + } + + @Override + public long avgRt() { + long rt = 0; + long totalQps = 0; + for (Node node : getChildList()) { + rt += node.avgRt() * node.passQps(); + totalQps += node.passQps(); + } + return rt / (totalQps == 0 ? 1 : totalQps); + } + + @Override + public long blockedQps() { + int blockQps = 0; + for (Node node : getChildList()) { + blockQps += node.blockedQps(); + } + return blockQps; + } + + @Override + public long blockedRequest() { + long r = 0; + for (Node node : getChildList()) { + r += node.blockedRequest(); + } + return r; + } + + @Override + public int curThreadNum() { + int r = 0; + for (Node node : getChildList()) { + r += node.curThreadNum(); + } + return r; + } + + @Override + public long totalQps() { + int r = 0; + for (Node node : getChildList()) { + r += node.totalQps(); + } + return r; + } + + @Override + public long successQps() { + int r = 0; + for (Node node : getChildList()) { + r += node.successQps(); + } + return r; + } + + @Override + public long passQps() { + int r = 0; + for (Node node : getChildList()) { + r += node.passQps(); + } + return r; + } + + @Override + public long totalRequest() { + long r = 0; + for (Node node : getChildList()) { + r += node.totalRequest(); + } + return r; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/IntervalProperty.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/IntervalProperty.java new file mode 100755 index 00000000..12c85cd0 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/IntervalProperty.java @@ -0,0 +1,60 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/*** + * QPS statistics interval. + * + * @author youji.zj + * @author jialiang.linjl + */ +public class IntervalProperty { + + public static volatile int INTERVAL = 1; + + public static void init(SentinelProperty dataSource) { + dataSource.addListener(new FlowIntervalPropertyListener()); + } + + private static class FlowIntervalPropertyListener implements PropertyListener { + @Override + public void configUpdate(Integer value) { + if (value == null) { + value = 1; + } + INTERVAL = value; + RecordLog.info("Init flow interval: " + INTERVAL); + } + + @Override + public void configLoad(Integer value) { + if (value == null) { + value = 1; + } + INTERVAL = value; + for (ClusterNode node : ClusterBuilderSlot.getClusterNodeMap().values()) { + node.reset(); + } + RecordLog.info("Flow interval change received: " + INTERVAL); + } + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java new file mode 100755 index 00000000..9c919b5d --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/Node.java @@ -0,0 +1,119 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import java.util.Map; + +import com.alibaba.csp.sentinel.node.metric.MetricNode; + +/** + * This class holds real-time statistics for a resource. + * + * @author qinan.qn + * @author leyou + */ +public interface Node { + + /** + * Incoming request per minute. + */ + long totalRequest(); + + long totalSuccess(); + + /** + * Blocked request count per minute. + */ + long blockedRequest(); + + /** + * Exception count per minute. + */ + long totalException(); + + /** + * Incoming request per second. + */ + long passQps(); + + /** + * Blocked request per second. + */ + long blockedQps(); + + /** + * Incoming request + block request per second. + */ + long totalQps(); + + /** + * Outgoing request per second. + */ + long successQps(); + + long maxSuccessQps(); + + /** + * Exception count per second. + */ + long exceptionQps(); + + /** + * Average response per second. + */ + long avgRt(); + + long minRt(); + + /** + * Current active thread. + */ + int curThreadNum(); + + /** + * Last seconds block QPS. + */ + long previousBlockQps(); + + /** + * Last window QPS. + */ + long previousPassQps(); + + Map metrics(); + + void addPassRequest(); + + void rt(long rt); + + void increaseBlockedQps(); + + void increaseExceptionQps(); + + void increaseThreadNum(); + + void decreaseThreadNum(); + + /** + * Reset the internal counter. + */ + void reset(); + + /** + * Debug only. + */ + void debug(); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/NodeBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/NodeBuilder.java new file mode 100755 index 00000000..97262fca --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/NodeBuilder.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * Builds new {@link DefaultNode} and {@link ClusterNode}. + * + * @author qinan.qn + */ +public interface NodeBuilder { + + DefaultNode buildTreeNode(ResourceWrapper id, ClusterNode clusterNode); + + ClusterNode buildClusterNode(); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/SampleCountProperty.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/SampleCountProperty.java new file mode 100755 index 00000000..effc7ac4 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/SampleCountProperty.java @@ -0,0 +1,51 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.property.SimplePropertyListener; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/** + * @author jialiang.linjl + */ +public class SampleCountProperty { + + public static volatile int sampleCount = 2; + + public static void init(SentinelProperty property) { + + try { + property.addListener(new SimplePropertyListener() { + @Override + public void configUpdate(Integer value) { + if (value != null) { + sampleCount = value; + // Reset the value. + for (ClusterNode node : ClusterBuilderSlot.getClusterNodeMap().values()) { + node.reset(); + } + } + RecordLog.info("Current SampleCount: " + sampleCount); + } + + }); + } catch (Exception e) { + RecordLog.info(e.getMessage(), e); + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java new file mode 100755 index 00000000..4f6eb275 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java @@ -0,0 +1,189 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric; +import com.alibaba.csp.sentinel.slots.statistic.metric.Metric; + +/** + * @author qinan.qn + * @author jialiang.linjl + */ +public class StatisticNode implements Node { + + private transient Metric rollingCounterInSecond = new ArrayMetric(1000 / SampleCountProperty.sampleCount, + IntervalProperty.INTERVAL); + + private transient Metric rollingCounterInMinute = new ArrayMetric(1000, 2 * 60); + + private AtomicInteger curThreadNum = new AtomicInteger(0); + + private long lastFetchTime = -1; + + @Override + public Map metrics() { + long currentTime = TimeUtil.currentTimeMillis(); + currentTime = currentTime - currentTime % 1000; + Map metrics = new ConcurrentHashMap(); + List minutes = rollingCounterInMinute.details(); + for (MetricNode node : minutes) { + if (node.getTimestamp() > lastFetchTime && node.getTimestamp() < currentTime) { + if (node.getPassedQps() != 0 || node.getBlockedQps() != 0) { + metrics.put(node.getTimestamp(), node); + lastFetchTime = node.getTimestamp(); + } + } + } + + return metrics; + } + + @Override + public void reset() { + rollingCounterInSecond = new ArrayMetric(1000 / SampleCountProperty.sampleCount, IntervalProperty.INTERVAL); + } + + @Override + public long totalRequest() { + long totalRequest = rollingCounterInMinute.pass() + rollingCounterInMinute.block(); + return totalRequest / 2; + } + + @Override + public long blockedRequest() { + return rollingCounterInMinute.block() / 2; + } + + @Override + public long blockedQps() { + return rollingCounterInSecond.block() / IntervalProperty.INTERVAL; + } + + @Override + public long previousBlockQps() { + return this.rollingCounterInMinute.previousWindowBlock(); + } + + @Override + public long previousPassQps() { + return this.rollingCounterInMinute.previousWindowPass(); + } + + @Override + public long totalQps() { + return passQps() + blockedQps(); + } + + @Override + public long totalSuccess() { + return rollingCounterInMinute.success() / 2; + } + + @Override + public long exceptionQps() { + return rollingCounterInSecond.exception() / IntervalProperty.INTERVAL; + } + + @Override + public long totalException() { + return rollingCounterInMinute.exception() / 2; + } + + @Override + public long passQps() { + return rollingCounterInSecond.pass() / IntervalProperty.INTERVAL; + } + + @Override + public long successQps() { + return rollingCounterInSecond.success() / IntervalProperty.INTERVAL; + } + + @Override + public long maxSuccessQps() { + return rollingCounterInSecond.maxSuccess() * SampleCountProperty.sampleCount; + } + + @Override + public long avgRt() { + long successCount = rollingCounterInSecond.success(); + if (successCount == 0) { + return 0; + } + + return rollingCounterInSecond.rt() / successCount; + } + + @Override + public long minRt() { + return rollingCounterInSecond.minRt(); + } + + @Override + public int curThreadNum() { + return curThreadNum.get(); + } + + @Override + public void addPassRequest() { + rollingCounterInSecond.addPass(); + rollingCounterInMinute.addPass(); + } + + @Override + public void rt(long rt) { + rollingCounterInSecond.addSuccess(); + rollingCounterInSecond.addRT(rt); + + rollingCounterInMinute.addSuccess(); + rollingCounterInMinute.addRT(rt); + } + + @Override + public void increaseBlockedQps() { + rollingCounterInSecond.addBlock(); + rollingCounterInMinute.addBlock(); + } + + @Override + public void increaseExceptionQps() { + rollingCounterInSecond.addException(); + rollingCounterInMinute.addException(); + + } + + @Override + public void increaseThreadNum() { + curThreadNum.incrementAndGet(); + } + + @Override + public void decreaseThreadNum() { + curThreadNum.decrementAndGet(); + } + + @Override + public void debug() { + rollingCounterInSecond.debugQps(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java new file mode 100755 index 00000000..ef0171e9 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricNode.java @@ -0,0 +1,189 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node.metric; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MetricNode { + + private long timestamp; + private long passedQps; + private long blockedQps; + private long successQps; + private long exception; + private long rt; + + private String resource; + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(long successQps) { + this.successQps = successQps; + } + + public long getPassedQps() { + return passedQps; + } + + public void setPassedQps(long passedQps) { + this.passedQps = passedQps; + } + + public long getException() { + return exception; + } + + public void setException(long exception) { + this.exception = exception; + } + + public long getBlockedQps() { + return blockedQps; + } + + public void setBlockedQps(long blockedQps) { + this.blockedQps = blockedQps; + } + + public long getRt() { + return rt; + } + + public void setRt(long rt) { + this.rt = rt; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + @Override + public String toString() { + return "MetricNode{" + + "timestamp=" + timestamp + + ", passedQps=" + passedQps + + ", blockedQs=" + blockedQps + + ", successQps=" + successQps + + ", exception=" + exception + + ", rt=" + rt + + ", resource='" + resource + '\'' + + '}'; + } + + /** + * To formatting string. All "|" in {@link #resource} will be replaced with "_", format is: + *
+ * + * timestamp|resource|passedQps|blockedQps|successQps|exception|rt + * + * + * @return string format of this. + */ + public String toThinString() { + StringBuilder sb = new StringBuilder(); + sb.append(timestamp).append("|"); + String legalName = resource.replaceAll("\\|", "_"); + sb.append(legalName).append("|"); + sb.append(passedQps).append("|"); + sb.append(blockedQps).append("|"); + sb.append(successQps).append("|"); + sb.append(exception).append("|"); + sb.append(rt); + return sb.toString(); + } + + /** + * Parse {@link MetricNode} from thin string, see {@link #toThinString()} ()} + * + * @param line + * @return + */ + public static MetricNode fromThinString(String line) { + MetricNode node = new MetricNode(); + String[] strs = line.split("\\|"); + node.setTimestamp(Long.parseLong(strs[0])); + node.setResource(strs[1]); + node.setPassedQps(Long.parseLong(strs[2])); + node.setBlockedQps(Long.parseLong(strs[3])); + node.setSuccessQps(Long.parseLong(strs[4])); + node.setException(Long.parseLong(strs[5])); + node.setRt(Long.parseLong(strs[6])); + return node; + } + + /** + * To formatting string. All "|" in {@link MetricNode#resource} will be replaced with "_", format is: + *
+ * + * timestamp|yyyy-MM-dd HH:mm:ss|resource|passedQps|blockedQps|successQps|exception|rt\n + * + * + * @return string format of this. + */ + public String toFatString() { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + StringBuilder sb = new StringBuilder(32); + sb.delete(0, sb.length()); + sb.append(getTimestamp()).append("|"); + sb.append(df.format(new Date(getTimestamp()))).append("|"); + String legalName = getResource().replaceAll("\\|", "_"); + sb.append(legalName).append("|"); + sb.append(getPassedQps()).append("|"); + sb.append(getBlockedQps()).append("|"); + sb.append(getSuccessQps()).append("|"); + sb.append(getException()).append("|"); + sb.append(getRt()); + sb.append('\n'); + return sb.toString(); + } + + /** + * Parse {@link MetricNode} from fat string, see {@link #toFatString()} + * + * @param line + * @return the {@link MetricNode} parsed. + */ + public static MetricNode fromFatString(String line) { + String[] strs = line.split("\\|"); + Long time = Long.parseLong(strs[0]); + MetricNode node = new MetricNode(); + node.setTimestamp(time); + node.setResource(strs[2]); + node.setPassedQps(Long.parseLong(strs[3])); + node.setBlockedQps(Long.parseLong(strs[4])); + node.setSuccessQps(Long.parseLong(strs[5])); + node.setException(Long.parseLong(strs[6])); + node.setRt(Long.parseLong(strs[7])); + return node; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricSearcher.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricSearcher.java new file mode 100755 index 00000000..8151d4b1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricSearcher.java @@ -0,0 +1,328 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node.metric; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * 从指定目录下找出所有的metric文件,并按照指定时间戳进行检索,参考{@link MetricSearcher#find(long, int)}。 + * 会借助索引以提高检索效率,参考{@link MetricWriter};还会在内部缓存上一次检索的文件指针,以便下一次顺序检索时 + * 减少读盘次数。 + * + * @author leyou + */ +public class MetricSearcher { + + private static final Charset defaultCharset = Charset.forName(SentinelConfig.charset()); + + private String baseDir; + private String baseFileName; + private Charset charset; + + private Position lastPosition = new Position(); + + /** + * avoid OOM in any case + */ + private static final int maxLinesReturn = 100000; + + /** + * @param baseDir metric文件所在目录 + * @param baseFileName metric文件名的关键字,比如 alihot-metrics.log + */ + public MetricSearcher(String baseDir, String baseFileName) { + this(baseDir, baseFileName, defaultCharset); + } + + /** + * @param baseDir metric文件所在目录 + * @param baseFileName metric文件名的关键字,比如 alihot-metrics.log + * @param charset + */ + public MetricSearcher(String baseDir, String baseFileName, Charset charset) { + if (baseDir == null) { + throw new IllegalArgumentException("baseDir can't be null"); + } + if (baseFileName == null) { + throw new IllegalArgumentException("baseFileName can't be null"); + } + if (charset == null) { + throw new IllegalArgumentException("charset can't be null"); + } + this.baseDir = baseDir; + if (!baseDir.endsWith(File.separator)) { + this.baseDir += File.separator; + } + this.baseFileName = baseFileName; + this.charset = charset; + } + + /** + * 从beginTime开始,检索recommendLines条(大概)记录。同一秒中的数据是原子的,不能分割成多次查询。 + * + * @param beginTimeMs 检索的最小时间戳 + * @param recommendLines 查询最多想得到的记录条数,返回条数会尽可能不超过这个数字。但是为保证每一秒的数据不被分割,有时候 + * 返回的记录条数会大于该数字。 + * @return + * @throws Exception + */ + public synchronized List find(long beginTimeMs, int recommendLines) throws Exception { + List fileNames = MetricWriter.listMetricFiles(baseDir, baseFileName); + int i = 0; + long offsetInIndex = 0; + if (validPosition(beginTimeMs)) { + i = fileNames.indexOf(lastPosition.metricFileName); + if (i == -1) { + i = 0; + } else { + offsetInIndex = lastPosition.offsetInIndex; + } + } + for (; i < fileNames.size(); i++) { + String fileName = fileNames.get(i); + long offset = findOffset(beginTimeMs, fileName, + MetricWriter.formIndexFileName(fileName), offsetInIndex); + offsetInIndex = 0; + if (offset != -1) { + return readMetrics(fileNames, i, offset, recommendLines); + } + } + return null; + } + + /** + * Find metric between [beginTimeMs, endTimeMs], both side inclusive. + * When identity is null, all metric between the time intervalMs will be read, otherwise, only the specific + * identity will be read. + */ + public synchronized List findByTimeAndResource(long beginTimeMs, long endTimeMs, String identity) + throws Exception { + List fileNames = MetricWriter.listMetricFiles(baseDir, baseFileName); + //RecordLog.info("pid=" + pid + ", findByTimeAndResource([" + beginTimeMs + ", " + endTimeMs + // + "], " + identity + ")"); + int i = 0; + long offsetInIndex = 0; + if (validPosition(beginTimeMs)) { + i = fileNames.indexOf(lastPosition.metricFileName); + if (i == -1) { + i = 0; + } else { + offsetInIndex = lastPosition.offsetInIndex; + } + } else { + //RecordLog.info("lastPosition is invalidate, will re iterate all files, pid = " + pid); + } + + for (; i < fileNames.size(); i++) { + String fileName = fileNames.get(i); + long offset = findOffset(beginTimeMs, fileName, + fileName + MetricWriter.METRIC_FILE_INDEX_SUFFIX, offsetInIndex); + offsetInIndex = 0; + if (offset != -1) { + return readMetricsByEndTime(fileNames, i, offset, endTimeMs, identity); + } + } + return null; + } + + /** + * 记录上一次读取的index文件位置和数值 + */ + private static final class Position { + String metricFileName; + String indexFileName; + /** + * 索引文件内的偏移 + */ + long offsetInIndex; + /** + * 索引文件中offsetInIndex位置上的数字,秒数。 + */ + long second; + } + + /** + * The position we cached is useful only when {@code beginTimeMs} is >= {@code lastPosition.second} + * and the index file exists and the second we cached is same as in the index file. + */ + private boolean validPosition(long beginTimeMs) { + if (beginTimeMs / 1000 < lastPosition.second) { + return false; + } + if (lastPosition.indexFileName == null) { + return false; + } + // index file dose not exits + if (!new File(lastPosition.indexFileName).exists()) { + return false; + } + FileInputStream in = null; + try { + in = new FileInputStream(lastPosition.indexFileName); + in.getChannel().position(lastPosition.offsetInIndex); + DataInputStream indexIn = new DataInputStream(in); + // timestamp(second) in the specific position == that we cached + return indexIn.readLong() == lastPosition.second; + } catch (Exception e) { + return false; + } finally { + if (in != null) { + try { + in.close(); + } catch (Exception ignore) { + } + } + } + } + + /** + * @return if should continue read, return true, else false. + */ + private boolean readMetricsInOneFileByEndTime(List list, String fileName, + long offset, long endTimeMs, String identity) throws Exception { + FileInputStream in = null; + long endSecond = endTimeMs / 1000; + try { + in = new FileInputStream(fileName); + in.getChannel().position(offset); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset)); + String line; + while ((line = reader.readLine()) != null) { + MetricNode node = MetricNode.fromFatString(line); + long currentSecond = node.getTimestamp() / 1000; + if (currentSecond <= endSecond) { + // read all + if (identity == null) { + list.add(node); + } else if (node.getResource().equals(identity)) { + list.add(node); + } + } else { + return false; + } + if (list.size() >= maxLinesReturn) { + return false; + } + } + } finally { + if (in != null) { + in.close(); + } + } + return true; + } + + private void readMetricsInOneFile(List list, String fileName, + long offset, int recommendLines) throws Exception { + //if(list.size() >= recommendLines){ + // return; + //} + long lastSecond = -1; + if (list.size() > 0) { + lastSecond = list.get(list.size() - 1).getTimestamp() / 1000; + } + FileInputStream in = null; + try { + in = new FileInputStream(fileName); + in.getChannel().position(offset); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset)); + String line; + while ((line = reader.readLine()) != null) { + MetricNode node = MetricNode.fromFatString(line); + long currentSecond = node.getTimestamp() / 1000; + + if (list.size() < recommendLines) { + list.add(node); + } else if (currentSecond == lastSecond) { + list.add(node); + } else { + break; + } + lastSecond = currentSecond; + } + } finally { + if (in != null) { + in.close(); + } + } + } + + private List readMetrics(List fileNames, int pos, + long offset, int recommendLines) throws Exception { + List list = new ArrayList(recommendLines); + readMetricsInOneFile(list, fileNames.get(pos++), offset, recommendLines); + while (list.size() < recommendLines && pos < fileNames.size()) { + readMetricsInOneFile(list, fileNames.get(pos++), 0, recommendLines); + } + return list; + } + + /** + * When identity is null, all metric between the time intervalMs will be read, otherwise, only the specific + * identity will be read. + */ + private List readMetricsByEndTime(List fileNames, int pos, + long offset, long endTimeMs, String identity) throws Exception { + List list = new ArrayList(1024); + if (readMetricsInOneFileByEndTime(list, fileNames.get(pos++), offset, endTimeMs, identity)) { + while (pos < fileNames.size() + && readMetricsInOneFileByEndTime(list, fileNames.get(pos++), 0, endTimeMs, identity)) { + } + } + return list; + } + + private long findOffset(long beginTime, String metricFileName, + String idxFileName, long offsetInIndex) throws Exception { + lastPosition.metricFileName = null; + lastPosition.indexFileName = null; + if (!new File(idxFileName).exists()) { + return -1; + } + long beginSecond = beginTime / 1000; + FileInputStream in = new FileInputStream(idxFileName); + in.getChannel().position(offsetInIndex); + DataInputStream indexIn = new DataInputStream(in); + long offset; + try { + long second; + lastPosition.offsetInIndex = in.getChannel().position(); + while ((second = indexIn.readLong()) < beginSecond) { + offset = indexIn.readLong(); + lastPosition.offsetInIndex = in.getChannel().position(); + } + offset = indexIn.readLong(); + lastPosition.metricFileName = metricFileName; + lastPosition.indexFileName = idxFileName; + lastPosition.second = second; + return offset; + } catch (EOFException ignore) { + return -1; + } finally { + indexIn.close(); + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java new file mode 100755 index 00000000..9222a7b6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricTimerListener.java @@ -0,0 +1,68 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node.metric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +public class MetricTimerListener implements Runnable { + + private static final MetricWriter metricWriter = new MetricWriter(SentinelConfig.singleMetricFileSize(), + SentinelConfig.totalMetricFileCount()); + + @Override + public void run() { + Map> maps = new TreeMap>(); + + // 每5秒打印一次,把丢弃的seconds都给丢掉。 + for (Entry e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) { + String name = e.getKey().getName(); + ClusterNode node = e.getValue(); + Map metrics = node.metrics(); + + for (Entry entry : metrics.entrySet()) { + long time = entry.getKey(); + MetricNode metricNode = entry.getValue(); + metricNode.setResource(name); + if (maps.get(time) == null) { + maps.put(time, new ArrayList()); + } + List nodes = maps.get(time); + nodes.add(entry.getValue()); + } + } + if (!maps.isEmpty()) { + for (Entry> entry : maps.entrySet()) { + try { + metricWriter.write(entry.getKey(), entry.getValue()); + } catch (Exception e) { + RecordLog.info("write metric error: ", e); + } + } + } + + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricWriter.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricWriter.java new file mode 100755 index 00000000..836d27b8 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/metric/MetricWriter.java @@ -0,0 +1,349 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.node.metric; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.util.PidUtil; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + * This class is responsible for writing {@link MetricNode} to disk: + *
    + *
  1. metric with the same second should write to the same file;
  2. + *
  3. single file size must be controlled;
  4. + *
  5. file name is like: {@code ${AppName}_pid-metrics.log.yyyy-MM-dd.[number]}
  6. + *
  7. metric of different day should in different file;
  8. + *
  9. every metric file is accompanied with an index file, which file name is {@code ${metricFileName}.idx}
  10. + *
+ * + * @author leyou + */ +public class MetricWriter { + + private static final String CHARSET = SentinelConfig.charset(); + public static final String METRIC_BASE_DIR = RecordLog.getLogBaseDir(); + public static final String METRIC_FILE_SUFFIX = "-metrics.log"; + public static final String METRIC_FILE_INDEX_SUFFIX = ".idx"; + public static final Comparator METRIC_FILENAME_CMP = new MetricFileNameComparator(); + + private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + /** + * 排除时差干扰 + */ + private long timeSecondBase; + private final StringBuilder sb = new StringBuilder(32); + private String baseDir; + private String baseFileName; + /** + * file must exist when writing + */ + private File curMetricFile; + private File curMetricIndexFile; + + private FileOutputStream outMetric; + private DataOutputStream outIndex; + private BufferedOutputStream outMetricBuf; + private long singleFileSize; + private int totalFileCount; + private boolean append = false; + private final int pid = PidUtil.getPid(); + + /** + * 秒级统计,忽略毫秒数。 + */ + private long lastSecond = -1; + + public MetricWriter(long singleFileSize) { + this(singleFileSize, 6); + } + + public MetricWriter(long singleFileSize, int totalFileCount) { + if (singleFileSize <= 0 || totalFileCount <= 0) { + throw new IllegalArgumentException(); + } + RecordLog.info("new MetricWriter, singleFileSize=" + singleFileSize + ", totalFileCount=" + totalFileCount); + this.baseDir = METRIC_BASE_DIR; + File dir = new File(baseDir); + if (!dir.exists()) { + dir.mkdirs(); + } + + long time = System.currentTimeMillis(); + this.lastSecond = time / 1000; + this.singleFileSize = singleFileSize; + this.totalFileCount = totalFileCount; + try { + this.timeSecondBase = df.parse("1970-01-01 00:00:00").getTime() / 1000; + } catch (Exception e) { + RecordLog.info("new MetricWriter error: ", e); + } + } + + /** + * 如果传入了time,就认为nodes中所有的时间时间戳都是time. + * + * @param time + * @param nodes + */ + public synchronized void write(long time, List nodes) throws Exception { + if (nodes == null) { + return; + } + for (MetricNode node : nodes) { + node.setTimestamp(time); + } + + String appName = SentinelConfig.getAppName(); + if (appName == null) { + appName = ""; + } + // first write, should create file + if (curMetricFile == null) { + baseFileName = formMetricFileName(appName, pid); + closeAndNewFile(nextFileNameOfDay(time)); + } + if (!(curMetricFile.exists() && curMetricIndexFile.exists())) { + closeAndNewFile(nextFileNameOfDay(time)); + } + + long second = time / 1000; + if (second < lastSecond) { + // 时间靠前的直接忽略,不应该发生。 + } else if (second == lastSecond) { + for (MetricNode node : nodes) { + outMetricBuf.write(node.toFatString().getBytes(CHARSET)); + } + outMetricBuf.flush(); + if (!validSize()) { + closeAndNewFile(nextFileNameOfDay(time)); + } + } else { + writeIndex(second, outMetric.getChannel().position()); + if (isNewDay(lastSecond, second)) { + closeAndNewFile(nextFileNameOfDay(time)); + for (MetricNode node : nodes) { + outMetricBuf.write(node.toFatString().getBytes(CHARSET)); + } + outMetricBuf.flush(); + if (!validSize()) { + closeAndNewFile(nextFileNameOfDay(time)); + } + } else { + for (MetricNode node : nodes) { + outMetricBuf.write(node.toFatString().getBytes(CHARSET)); + } + outMetricBuf.flush(); + if (!validSize()) { + closeAndNewFile(nextFileNameOfDay(time)); + } + } + lastSecond = second; + } + } + + public synchronized void close() throws Exception { + if (outMetricBuf != null) { + outMetricBuf.close(); + } + if (outIndex != null) { + outIndex.close(); + } + } + + private void writeIndex(long time, long offset) throws Exception { + outIndex.writeLong(time); + outIndex.writeLong(offset); + outIndex.flush(); + } + + private String nextFileNameOfDay(long time) { + List list = new ArrayList(); + File baseFile = new File(baseDir); + DateFormat fileNameDf = new SimpleDateFormat("yyyy-MM-dd"); + String dateStr = fileNameDf.format(new Date(time)); + for (File file : baseFile.listFiles()) { + if (file.getName().contains(baseFileName + "." + dateStr) + && !file.getName().endsWith(METRIC_FILE_INDEX_SUFFIX) + && !file.getName().endsWith(".lck")) { + list.add(file.getAbsolutePath()); + } + } + Collections.sort(list, METRIC_FILENAME_CMP); + if (list.isEmpty()) { + return baseDir + baseFileName + "." + dateStr; + } + String last = list.get(list.size() - 1); + int n = 0; + String[] strs = last.split("\\."); + if (strs.length > 0 && strs[strs.length - 1].matches("[0-9]{1,10}")) { + n = Integer.parseInt(strs[strs.length - 1]); + } + return baseDir + baseFileName + "." + dateStr + "." + (n + 1); + } + + /** + * A comparator for metric file name. Metric file name is like:
+ *
+     * aliswitch-8728-metrics.log.2018-03-06
+     * aliswitch-8728-metrics.log.2018-03-07
+     * aliswitch-8728-metrics.log.2018-03-07.10
+     * aliswitch-8728-metrics.log.2018-03-06.100
+     * 
+ *

+ * File name with the early date is smaller, if date is same, the one with the small file number is smaller. + * Note that if the name is an absolute path, only the fileName({@link File#getName()}) part will be considered. + * So the above file names should be sorted as:
+ *

+     * aliswitch-8728-metrics.log.2018-03-06
+     * aliswitch-8728-metrics.log.2018-03-06.100
+     * aliswitch-8728-metrics.log.2018-03-07
+     * aliswitch-8728-metrics.log.2018-03-07.10
+     * 
+ *

+ */ + private static final class MetricFileNameComparator implements Comparator { + @Override + public int compare(String o1, String o2) { + String name1 = new File(o1).getName(); + String name2 = new File(o2).getName(); + String dateStr1 = name1.split("\\.")[2]; + String dateStr2 = name2.split("\\.")[2]; + + // compare date first + int t = dateStr1.compareTo(dateStr2); + if (t != 0) { + return t; + } + + // same date, compare file number + t = name1.length() - name2.length(); + if (t != 0) { + return t; + } + return name1.compareTo(name2); + } + } + + /** + * Get all metric files' name in {@code baseDir}. The file name must contain {@code baseFileName} + * and not endsWith {@link #METRIC_FILE_INDEX_SUFFIX} or ".lck". + * + * @param baseDir the directory to search. + * @param baseFileName the file name pattern. + * @return the metric files' absolute path({@link File#getAbsolutePath()}) + * @throws Exception + */ + static List listMetricFiles(String baseDir, String baseFileName) throws Exception { + List list = new ArrayList(); + File baseFile = new File(baseDir); + File[] files = baseFile.listFiles(); + if (files == null) { + return list; + } + for (File file : files) { + if (file.isFile() + && file.getName().contains(baseFileName) + && !file.getName().endsWith(MetricWriter.METRIC_FILE_INDEX_SUFFIX) + && !file.getName().endsWith(".lck")) { + list.add(file.getAbsolutePath()); + } + } + Collections.sort(list, MetricWriter.METRIC_FILENAME_CMP); + return list; + } + + private void removeMoreFiles() throws Exception { + List list = listMetricFiles(baseDir, baseFileName); + if (list == null || list.isEmpty()) { + return; + } + for (int i = 0; i < list.size() - totalFileCount + 1; i++) { + String fileName = list.get(i); + String indexFile = formIndexFileName(fileName); + new File(fileName).delete(); + RecordLog.info("remove metric file: " + fileName); + new File(indexFile).delete(); + RecordLog.info("remove metric index file: " + indexFile); + } + } + + private void closeAndNewFile(String fileName) throws Exception { + removeMoreFiles(); + if (outMetricBuf != null) { + outMetricBuf.close(); + } + if (outIndex != null) { + outIndex.close(); + } + outMetric = new FileOutputStream(fileName, append); + outMetricBuf = new BufferedOutputStream(outMetric); + curMetricFile = new File(fileName); + String idxFile = formIndexFileName(fileName); + ; + curMetricIndexFile = new File(idxFile); + outIndex = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(idxFile, append))); + RecordLog.info("create new metric file: " + fileName); + RecordLog.info("create new metric index file: " + idxFile); + } + + private boolean validSize() throws Exception { + long size = outMetric.getChannel().size(); + return size < singleFileSize; + } + + private boolean isNewDay(long lastSecond, long second) { + long lastDay = (lastSecond - timeSecondBase) / 86400; + long newDay = (second - timeSecondBase) / 86400; + return newDay > lastDay; + } + + /** + * Form metric file name use the specific appName and pid. Not that only + * form the file name, not include path. + * + * @param appName + * @param pid + * @return metric file name. + */ + public static String formMetricFileName(String appName, int pid) { + if (appName == null) { + appName = ""; + } + return appName + "-" + pid + METRIC_FILE_SUFFIX; + } + + /** + * Form index file name of the {@code metricFileName} + * + * @param metricFileName + * @return the index file name of the metricFileName + */ + public static String formIndexFileName(String metricFileName) { + return metricFileName + METRIC_FILE_INDEX_SUFFIX; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/DynamicSentinelProperty.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/DynamicSentinelProperty.java new file mode 100755 index 00000000..50716397 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/DynamicSentinelProperty.java @@ -0,0 +1,77 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.property; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.alibaba.csp.sentinel.log.RecordLog; + +public class DynamicSentinelProperty implements SentinelProperty { + + protected Set> listeners = Collections.synchronizedSet(new HashSet>()); + private T value = null; + + public DynamicSentinelProperty() { + } + + public DynamicSentinelProperty(T value) { + super(); + this.value = value; + } + + @Override + public void addListener(PropertyListener listener) { + listeners.add(listener); + listener.configLoad(value); + } + + @Override + public void removeListener(PropertyListener listener) { + listeners.remove(listener); + } + + @Override + public void updateValue(T newValue) { + if (isEqual(value, newValue)) { + return; + } + RecordLog.info("SentinelProperty, config is real updated to: " + newValue); + + value = newValue; + for (PropertyListener listener : listeners) { + listener.configUpdate(newValue); + } + + } + + public boolean isEqual(T oldValue, T newValue) { + if (oldValue == null && newValue == null) { + return true; + } + + if (oldValue == null && newValue != null) { + return false; + } + + return oldValue.equals(newValue); + } + + public void close() { + listeners.clear(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/NoOpSentinelProperty.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/NoOpSentinelProperty.java new file mode 100755 index 00000000..c1a704fc --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/NoOpSentinelProperty.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.property; + +/** + * A {@link SentinelProperty} that will never inform the {@link PropertyListener} on it. + * + * @author leyou + */ +public final class NoOpSentinelProperty implements SentinelProperty { + @Override + public void addListener(PropertyListener listener) { } + + @Override + public void removeListener(PropertyListener listener) { } + + @Override + public void updateValue(Object newValue) { } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/PropertyListener.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/PropertyListener.java new file mode 100755 index 00000000..69e4c960 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/PropertyListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.property; + +/** + * This class holds callback method when {@link SentinelProperty#updateValue(Object)} need inform the listener + * + * @author jialiang.linjl + */ +public interface PropertyListener { + + /** + * Callback method when {@link SentinelProperty#updateValue(Object)} need inform the listener. + * + * @param value updated value. + */ + void configUpdate(T value); + + /** + * The first time of the {@code value}'s load. + * + * @param value the value loaded. + */ + void configLoad(T value); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SentinelProperty.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SentinelProperty.java new file mode 100755 index 00000000..c71d9eb5 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SentinelProperty.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.property; + +/** + *

+ * This class holds current value of the config, and is responsible for informing all {@link PropertyListener}s + * added on this when the config is updated. + *

+ *

+ * Note that not every {@link #updateValue(Object newValue)} invocation should inform the listeners, only when + * {@code newValue} is not Equals to the old value, informing is needed. + *

+ * + * @param the target type. + * @author Carpenter Lee + */ +public interface SentinelProperty { + + /** + *

+ * Add a {@link PropertyListener} to this {@link SentinelProperty}. After the listener is added, + * {@link #updateValue(Object)} will inform the listener if needed. + *

+ *

+ * This method can invoke multi times to add more than one listeners. + *

+ * + * @param listener listener to add. + */ + void addListener(PropertyListener listener); + + /** + * Remove the {@link PropertyListener} on this. After removing, {@link #updateValue(Object)} + * will not inform the listener. + * + * @param listener the listener to remove. + */ + void removeListener(PropertyListener listener); + + /** + * Update the {@code newValue} as the current value of this property and inform all {@link PropertyListener}s + * added on this only when new {@code newValue} is not Equals to the old value. + * + * @param newValue the new value. + */ + void updateValue(T newValue); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SimplePropertyListener.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SimplePropertyListener.java new file mode 100755 index 00000000..0e950db1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/property/SimplePropertyListener.java @@ -0,0 +1,24 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.property; + +public abstract class SimplePropertyListener implements PropertyListener { + + @Override + public void configLoad(T value) { + configUpdate(value); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/AbstractLinkedProcessorSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/AbstractLinkedProcessorSlot.java new file mode 100755 index 00000000..3d10186f --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/AbstractLinkedProcessorSlot.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +import com.alibaba.csp.sentinel.context.Context; + +/** + * @author qinan.qn + * @author jialiang.linjl + */ +public abstract class AbstractLinkedProcessorSlot implements ProcessorSlot { + + private AbstractLinkedProcessorSlot next = null; + + @Override + public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) + throws Throwable { + if (next != null) { + next.transformEntry(context, resourceWrapper, obj, count, args); + } + } + + @SuppressWarnings("unchecked") + void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, Object... args) + throws Throwable { + T t = (T)o; + entry(context, resourceWrapper, t, count, args); + } + + @Override + public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + if (next != null) { + next.exit(context, resourceWrapper, count, args); + } + } + + public AbstractLinkedProcessorSlot getNext() { + return next; + } + + public void setNext(AbstractLinkedProcessorSlot next) { + this.next = next; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java new file mode 100755 index 00000000..b48301e2 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/DefaultProcessorSlotChain.java @@ -0,0 +1,83 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +import com.alibaba.csp.sentinel.context.Context; + +/** + * @author qinan.qn + * @author jialiang.linjl + */ +public class DefaultProcessorSlotChain extends ProcessorSlotChain { + + AbstractLinkedProcessorSlot first = new AbstractLinkedProcessorSlot() { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) + throws Throwable { + super.fireEntry(context, resourceWrapper, t, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + super.fireExit(context, resourceWrapper, count, args); + } + + }; + AbstractLinkedProcessorSlot end = first; + + @Override + public void addFirst(AbstractLinkedProcessorSlot protocolProcessor) { + protocolProcessor.setNext(first.getNext()); + first.setNext(protocolProcessor); + if (end == first) { + end = protocolProcessor; + } + } + + @Override + public void addLast(AbstractLinkedProcessorSlot protocolProcessor) { + end.setNext(protocolProcessor); + end = protocolProcessor; + } + + /** + * Same as {@link #addLast(AbstractLinkedProcessorSlot)}. + * + * @param next processor to be added. + */ + @Override + public void setNext(AbstractLinkedProcessorSlot next) { + addLast(next); + } + + @Override + public AbstractLinkedProcessorSlot getNext() { + return first.getNext(); + } + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, Object... args) + throws Throwable { + first.transformEntry(context, resourceWrapper, t, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + first.exit(context, resourceWrapper, count, args); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java new file mode 100755 index 00000000..4d8b4c33 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/MethodResourceWrapper.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +import java.lang.reflect.Method; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.util.IdUtil; +import com.alibaba.csp.sentinel.util.MethodUtil; + +/** + * Resource wrapper for method invocation. + * + * @author qinan.qn + */ +public class MethodResourceWrapper extends ResourceWrapper { + + private transient Method method; + + public MethodResourceWrapper(Method method, EntryType type) { + this.method = method; + this.name = MethodUtil.getMethodName(method); + this.type = type; + } + + @Override + public String getName() { + return name; + } + + public Method getMethod() { + return method; + } + + @Override + public String getShowName() { + return IdUtil.truncate(this.name); + } + + @Override + public EntryType getType() { + return type; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java new file mode 100755 index 00000000..d4f0af5b --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlot.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +import com.alibaba.csp.sentinel.context.Context; + +/** + * A container of some process and ways of notification when the process is finished. + * + * @author qinan.qn + * @author jialiang.linjl + * @author leyou(lihao) + */ +public interface ProcessorSlot { + + /** + * Entrance of this slot. + * + * @param context current {@link Context} + * @param resourceWrapper current resource + * @param param Generics parameter, usually is a {@link com.alibaba.csp.sentinel.node.Node} + * @param count tokens needed + * @param args parameters of the original call + * @throws Throwable + */ + void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, Object... args) + throws Throwable; + + /** + * Means finish of {@link #entry(Context, ResourceWrapper, Object, int, Object...)}. + * + * @param context current {@link Context} + * @param resourceWrapper current resource + * @param obj + * @param count tokens needed + * @param args parameters of the original call + * @throws Throwable + */ + void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) + throws Throwable; + + /** + * Exit of this slot. + * + * @param context current {@link Context} + * @param resourceWrapper current resource + * @param count tokens needed + * @param args parameters of the original call + */ + void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args); + + /** + * Means finish of {@link #exit(Context, ResourceWrapper, int, Object...)}. + * + * @param context current {@link Context} + * @param resourceWrapper current resource + * @param count tokens needed + * @param args parameters of the original call + */ + void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotChain.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotChain.java new file mode 100755 index 00000000..5a17681e --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ProcessorSlotChain.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +/** + * Link all processor slots as a chain. + * + * @author qinan.qn + */ +public abstract class ProcessorSlotChain extends AbstractLinkedProcessorSlot { + + /** + * Add a processor to the head of this slot chain. + * + * @param protocolProcessor processor to be added. + */ + public abstract void addFirst(AbstractLinkedProcessorSlot protocolProcessor); + + /** + * Add a processor to the tail of this slot chain. + * + * @param protocolProcessor processor to be added. + */ + public abstract void addLast(AbstractLinkedProcessorSlot protocolProcessor); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java new file mode 100755 index 00000000..a796344c --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/ResourceWrapper.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +import com.alibaba.csp.sentinel.EntryType; + +/** + * A wrapper of resource name and {@link EntryType}. + * + * @author qinan.qn + * @author jialiang.linjl + */ +public abstract class ResourceWrapper { + + protected String name; + protected EntryType type = EntryType.OUT; + + public abstract String getName(); + + public abstract String getShowName(); + + /** + * Get {@link EntryType} of this wrapper. + * + * @return {@link EntryType} of this wrapper. + */ + public abstract EntryType getType(); + + /** + * Only {@link #getName()} is considered. + */ + @Override + public int hashCode() { + return getName().hashCode(); + } + + /** + * Only {@link #getName()} is considered. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof ResourceWrapper) { + ResourceWrapper rw = (ResourceWrapper)obj; + return rw.getName().equals(getName()); + } + return false; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java new file mode 100755 index 00000000..a9096356 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/StringResourceWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slotchain; + +import com.alibaba.csp.sentinel.EntryType; + +/** + * Common resource wrapper. + * + * @author qinan.qn + * @author jialiang.linjl + */ +public class StringResourceWrapper extends ResourceWrapper { + + public StringResourceWrapper(String name, EntryType type) { + if (name == null) { + throw new IllegalArgumentException("Resource name cannot be null"); + } + this.name = name; + this.type = type; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getShowName() { + return name; + } + + @Override + public EntryType getType() { + return type; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotsChainBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotsChainBuilder.java new file mode 100755 index 00000000..5da3d5fa --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotsChainBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots; + +import com.alibaba.csp.sentinel.slotchain.DefaultProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; +import com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot; +import com.alibaba.csp.sentinel.slots.block.flow.FlowSlot; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.slots.logger.LogSlot; +import com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot; +import com.alibaba.csp.sentinel.slots.statistic.StatisticSlot; +import com.alibaba.csp.sentinel.slots.system.SystemSlot; + +/** + * Helper class to create {@link ProcessorSlotChain}. + * + * @author qinan.qn + * @author leyou + */ +public class DefaultSlotsChainBuilder implements SlotsChainBuilder { + + @Override + public ProcessorSlotChain build() { + ProcessorSlotChain chain = new DefaultProcessorSlotChain(); + chain.addLast(new NodeSelectorSlot()); + chain.addLast(new ClusterBuilderSlot()); + chain.addLast(new LogSlot()); + chain.addLast(new StatisticSlot()); + chain.addLast(new SystemSlot()); + chain.addLast(new AuthoritySlot()); + chain.addLast(new FlowSlot()); + chain.addLast(new DegradeSlot()); + + return chain; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/SlotsChainBuilder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/SlotsChainBuilder.java new file mode 100755 index 00000000..59db01a6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/SlotsChainBuilder.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots; + +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; + +/** + * @author qinan.qn + * @author leyou + */ +public interface SlotsChainBuilder { + + /** + * Helper method to create processor slot chain. + * + * @return a processor slot that chain some slots together. + */ + ProcessorSlotChain build(); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/AbstractRule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/AbstractRule.java new file mode 100755 index 00000000..ea7e7a3f --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/AbstractRule.java @@ -0,0 +1,94 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block; + +/*** + * @author youji.zj + */ +public abstract class AbstractRule implements Rule { + + /*** 规则的资源描述 ***/ + private String resource; + + /*** 被限制的应用,授权时候为逗号分隔的应用集合,限流时为单个应用 ***/ + private String limitApp; + + public String getResource() { + return resource; + } + + public AbstractRule setResource(String resource) { + this.resource = resource; + return this; + } + + public String getLimitApp() { + return limitApp; + } + + public AbstractRule setLimitApp(String limitApp) { + this.limitApp = limitApp; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AbstractRule)) { + return false; + } + + AbstractRule that = (AbstractRule)o; + + if (resource != null ? !resource.equals(that.resource) : that.resource != null) { + return false; + } + // if (limitApp != null ? !limitApp.equals(that.limitApp) : + // that.limitApp != null) { return false; } + if (!limitAppEquals(limitApp, that.limitApp)) { + return false; + } + return true; + } + + private boolean limitAppEquals(String str1, String str2) { + if ("".equals(str1)) { + return "default".equals(str2); + } else if ("default".equals(str1)) { + return "".equals(str2) || str2 == null || str1.equals(str2); + } + if (str1 == null) { + return str2 == null || "default".equals(str2); + } + return str1.equals(str2); + } + + public T as(Class clazz) { + return (T)this; + } + + @Override + public int hashCode() { + int result = resource != null ? resource.hashCode() : 0; + // result = 31 * result + (limitApp != null ? limitApp.hashCode() : 0); + if (!("".equals(limitApp) || "default".equals(limitApp) || limitApp == null)) { + result = 31 * result + limitApp.hashCode(); + } + return result; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/BlockException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/BlockException.java new file mode 100755 index 00000000..605b7b7c --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/BlockException.java @@ -0,0 +1,101 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block; + +/*** + * Abstract exception indicating blocked by Sentinel due to flow control, degraded or system guard. + * + * @author youji.zj + */ +public abstract class BlockException extends Exception { + + public static final String BLOCK_EXCEPTION_FLAG = "SentinelBlockException"; + /** + *

this constant RuntimeException has no stack trace, just has a message + * {@link #BLOCK_EXCEPTION_FLAG} that marks its name. + *

+ *

+ * Use {@link #isBlockException(Throwable)} to check whether one Exception + * Sentinel Blocked Exception. + *

+ */ + public static RuntimeException THROW_OUT_EXCEPTION = new RuntimeException(BLOCK_EXCEPTION_FLAG); + + public static StackTraceElement[] sentinelStackTrace = new StackTraceElement[] { + new StackTraceElement(BlockException.class.getName(), "block", "BlockException", 0) + }; + + static { + THROW_OUT_EXCEPTION.setStackTrace(sentinelStackTrace); + } + + private String ruleLimitApp; + + public BlockException(String ruleLimitApp) { + super(); + this.ruleLimitApp = ruleLimitApp; + } + + public BlockException(String message, Throwable cause) { + super(message, cause); + } + + public BlockException(String ruleLimitApp, String message) { + super(message); + this.ruleLimitApp = ruleLimitApp; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + + public String getRuleLimitApp() { + return ruleLimitApp; + } + + public void setRuleLimitApp(String ruleLimitApp) { + this.ruleLimitApp = ruleLimitApp; + } + + /** + * Check whether the exception is sentinel blocked exception. One exception is sentinel blocked + * exception only when: + *
    + *
  • the exception or its (sub-)cause is {@link BlockException}, or
  • + *
  • the exception's message is or any of its sub-cause's message equals to {@link #BLOCK_EXCEPTION_FLAG}
  • + *
+ * + * @param t the exception. + * @return return true if the exception marks sentinel blocked exception. + */ + public static boolean isBlockException(Throwable t) { + if (null == t) { + return false; + } + + int counter = 0; + Throwable cause = t; + while (cause != null && counter++ < 50) { + if ((cause instanceof BlockException) || (BLOCK_EXCEPTION_FLAG.equals(cause.getMessage()))) { + return true; + } + cause = cause.getCause(); + } + + return false; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/Rule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/Rule.java new file mode 100755 index 00000000..e429b2a5 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/Rule.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; + +/** + * Base interface of all rules. + * + * @author youji.zj + */ +public interface Rule { + + /** + * Check whether current statistical indicators meet this rule, which means not exceeding any threshold. + * + * @param context current {@link Context} + * @param node current {@link com.alibaba.csp.sentinel.node.Node} + * @param count tokens needed. + * @param args arguments of the original invocation. + * @return If current statistical indicators not exceeding any threshold return true, otherwise return false. + */ + boolean passCheck(Context context, DefaultNode node, int count, Object... args); + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java new file mode 100755 index 00000000..645b38a6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/RuleConstant.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block; + +/*** + * @author youji.zj + * @author jialiang.linjl + */ +public class RuleConstant { + + public static final int FLOW_GRADE_THREAD = 0; + public static final int FLOW_GRADE_QPS = 1; + + public static final int DEGRADE_GRADE_RT = 0; + public static final int DEGRADE_GRADE_EXCEPTION = 1; + + public static final int WHILE = 0; + public static final int BLACK = 1; + + public static final int STRATEGY_DIRECT = 0; + public static final int STRATEGY_RELATE = 1; + public static final int STRATEGY_CHAIN = 2; + + public static final int CONTROL_BEHAVIOR_DEFAULT = 0; + public static final int CONTROL_BEHAVIOR_WARM_UP = 1; + public static final int CONTROL_BEHAVIOR_RATE_LIMITER = 2; + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/SentinelRpcException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/SentinelRpcException.java new file mode 100755 index 00000000..1025fee7 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/SentinelRpcException.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block; + +/** + * A {@link RuntimeException} marks sentinel RPC exception. The stack trace + * is removed for high performance. + * + * @author leyou + */ +public class SentinelRpcException extends RuntimeException { + + public SentinelRpcException(String msg) { + super(msg); + } + + public SentinelRpcException(Throwable e) { + super(e); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityException.java new file mode 100755 index 00000000..e007ec65 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityException.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.authority; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * + * @author youji.zj + */ +public class AuthorityException extends BlockException { + + public AuthorityException(String ruleLimitApp) { + super(ruleLimitApp); + } + + public AuthorityException(String message, Throwable cause) { + super(message, cause); + } + + public AuthorityException(String ruleLimitApp, String message) { + super(ruleLimitApp, message); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRule.java new file mode 100755 index 00000000..ea97f6b8 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRule.java @@ -0,0 +1,104 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.authority; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +/*** + * + * @author youji.zj + */ +public class AuthorityRule extends AbstractRule { + + /*** 0代表白名单;1代表黑名单 ***/ + private int strategy; + + public int getStrategy() { + return strategy; + } + + public void setStrategy(int strategy) { + this.strategy = strategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (!(o instanceof AuthorityRule)) { return false; } + if (!super.equals(o)) { return false; } + + AuthorityRule rule = (AuthorityRule)o; + + return strategy == rule.strategy; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + strategy; + return result; + } + + @Override + public boolean passCheck(Context context, DefaultNode node, int count, Object... args) { + String requester = context.getOrigin(); + + // 来源或者限流的应用为null直接通过 + if (StringUtil.isEmpty(requester) || this.getLimitApp() == null) { + return true; + } + + // 白名单、黑名单列表为逗号分隔的应用列表, indexOf还不行,需要精确匹配 + int pos = this.getLimitApp().indexOf(requester); + boolean contain = pos > -1; + + if (contain) { + boolean exactlyMatch = false; + String[] appArray = this.getLimitApp().split(","); + for (String app : appArray) { + if (requester.equals(app)) { + exactlyMatch = true; + break; + } + } + + contain = exactlyMatch; + } + + if (strategy == RuleConstant.BLACK && contain) { + return false; + } + + if (strategy == RuleConstant.WHILE && !contain) { + return false; + } + + return true; + } + + @Override + public String toString() { + return "AuthorityRule{" + + "resource=" + getResource() + + ", limitApp=" + getLimitApp() + + ", strategy=" + strategy + + "} " + super.toString(); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRuleManager.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRuleManager.java new file mode 100755 index 00000000..2c762282 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRuleManager.java @@ -0,0 +1,156 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.authority; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +/*** + * @author youji.zj + * @author jialiang.linjl + */ +public class AuthorityRuleManager { + + private static Map> authorityRules + = new ConcurrentHashMap>(); + + final static RulePropertyListener listener = new RulePropertyListener(); + + private static SentinelProperty> currentProperty + = new DynamicSentinelProperty>(); + + static { + currentProperty.addListener(listener); + } + + public static void register2Property(SentinelProperty> property) { + synchronized (listener) { + if (currentProperty != null) { + currentProperty.removeListener(listener); + } + property.addListener(listener); + currentProperty = property; + } + } + + /** + * @param rules + */ + public static void loadRules(List rules) { + currentProperty.updateValue(rules); + } + + public static void checkAuthority(ResourceWrapper resource, Context context, DefaultNode node, int count) + throws BlockException { + if (authorityRules == null) { + return; + } + + List rules = authorityRules.get(resource.getName()); + if (rules == null) { + return; + } + + for (AuthorityRule rule : rules) { + if (!rule.passCheck(context, node, count)) { + throw new AuthorityException(context.getOrigin()); + } + } + } + + public static boolean hasConfig(String resource) { + return authorityRules.containsKey(resource); + } + + /** + * Get a copy of the rules. + * + * @return a new copy of the rules. + */ + public static List getRules() { + List rules = new ArrayList(); + if (authorityRules == null) { + return rules; + } + for (Map.Entry> entry : authorityRules.entrySet()) { + rules.addAll(entry.getValue()); + } + return rules; + } + + private static class RulePropertyListener implements PropertyListener> { + + @Override + public void configUpdate(List conf) { + Map> rules = loadAuthorityConf(conf); + + authorityRules.clear(); + if (rules != null) { + authorityRules.putAll(rules); + } + RecordLog.info("receive authority config: " + authorityRules); + } + + private Map> loadAuthorityConf(List list) { + if (list == null) { + return null; + } + Map> newRuleMap = new ConcurrentHashMap>(); + for (AuthorityRule rule : list) { + if (StringUtil.isBlank(rule.getLimitApp())) { + rule.setLimitApp(FlowRule.DEFAULT); + } + + String identity = rule.getResource(); + List ruleM = newRuleMap.get(identity); + if (ruleM == null) { + ruleM = new ArrayList(); + newRuleMap.put(identity, ruleM); + } + ruleM.add(rule); + + } + + return newRuleMap; + } + + @Override + public void configLoad(List value) { + Map> rules = loadAuthorityConf(value); + + authorityRules.clear(); + if (rules != null) { + authorityRules.putAll(rules); + } + RecordLog.info("load authority config: " + authorityRules); + } + + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthoritySlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthoritySlot.java new file mode 100755 index 00000000..1101d891 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthoritySlot.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.authority; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * A {@link ProcessorSlot} that dedicates to {@link AuthorityRule} checking. + * + * @author leyou + */ +public class AuthoritySlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + AuthorityRuleManager.checkAuthority(resourceWrapper, context, node, count); + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeException.java new file mode 100755 index 00000000..1538373d --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeException.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.degrade; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * @author youji.zj + */ +public class DegradeException extends BlockException { + + public DegradeException(String ruleLimitApp) { + super(ruleLimitApp); + } + + public DegradeException(String message, Throwable cause) { + super(message, cause); + } + + public DegradeException(String ruleLimitApp, String message) { + super(ruleLimitApp, message); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java new file mode 100755 index 00000000..48b9a810 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java @@ -0,0 +1,238 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.degrade; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/** + *

+ * Degrade is used when the resources are in an unstable state, these resources + * will be degraded within the next defined time window. There are two ways to + * measure whether a resource is stable or not: + *

+ *
    + *
  • + * Average response time ({@code DEGRADE_GRADE_RT}): When + * the average RT exceeds the threshold ('count' in 'DegradeRule', in milliseconds), the + * resource enters a quasi-degraded state. If the RT of next coming 5 + * requests still exceed this threshold, this resource will be downgraded, which + * means that in the next time window (defined in 'timeWindow', in seconds) all the + * access to this resource will be blocked. + *
  • + *
  • + * Exception ratio: When the ratio of exception count per second and the + * success qps exceeds the threshold, access to the resource will be blocked in + * the coming window. + *
  • + *
+ * + * @author jialiang.linjl + */ +public class DegradeRule extends AbstractRule { + + private static final int RT_MAX_EXCEED_N = 5; + + private static ScheduledExecutorService pool = Executors.newScheduledThreadPool( + Runtime.getRuntime().availableProcessors()); + + /** + * RT threshold or exception ratio threshold count. + */ + private double count; + + /** + * Degrade recover timeout (in seconds) when degradation occurs. + */ + private int timeWindow; + + /** + * Degrade strategy (0: average RT, 1: exception ratio). + */ + private int grade = RuleConstant.DEGRADE_GRADE_RT; + + private volatile boolean cut = false; + + public int getGrade() { + return grade; + } + + public void setGrade(int grade) { + this.grade = grade; + } + + private AtomicLong passCount = new AtomicLong(0); + + private final Object lock = new Object(); + + public double getCount() { + return count; + } + + public void setCount(double count) { + this.count = count; + } + + public boolean isCut() { + return cut; + } + + public void setCut(boolean cut) { + this.cut = cut; + } + + public AtomicLong getPassCount() { + return passCount; + } + + public void setPassCount(AtomicLong passCount) { + this.passCount = passCount; + } + + public int getTimeWindow() { + return timeWindow; + } + + public void setTimeWindow(int timeWindow) { + this.timeWindow = timeWindow; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DegradeRule)) { + return false; + } + if (!super.equals(o)) { + return false; + } + + DegradeRule that = (DegradeRule)o; + + if (count != that.count) { + return false; + } + if (timeWindow != that.timeWindow) { + return false; + } + if (grade != that.grade) { + return false; + } + // if (cut != that.cut) { return false; } + //// AtomicLong dose not Override equals() + // if ((passCount == null && that.passCount != null) + // || (passCount.get() != that.passCount.get())) { + // return false; + // } + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + new Double(count).hashCode(); + result = 31 * result + timeWindow; + result = 31 * result + grade; + // result = 31 * result + (cut ? 1 : 0); + // result = 31 * result + (passCount != null ? (int)passCount.get() : + // 0); + return result; + } + + @Override + public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) { + + if (cut) { + return false; + } + + ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource()); + if (clusterNode == null) { + return true; + } + + if (grade == RuleConstant.DEGRADE_GRADE_RT) { + double rt = clusterNode.avgRt(); + if (rt < this.count) { + return true; + } + + // Sentinel will degrade the service only if count exceeds. + if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) { + return true; + } + } else { + double exception = clusterNode.exceptionQps(); + double success = clusterNode.successQps(); + if (success == 0) { + return true; + } + + if (exception / success < count) { + return true; + } + } + + synchronized (lock) { + if (!cut) { + // Automatically degrade. + cut = true; + ResetTask resetTask = new ResetTask(this); + pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS); + } + + return false; + } + } + + @Override + public String toString() { + return "DegradeRule{" + + "resource=" + getResource() + + ", grade=" + grade + + ", count=" + count + + ", limitApp=" + getLimitApp() + + ", timeWindow=" + timeWindow + + "}"; + } + + private static final class ResetTask implements Runnable { + + private DegradeRule rule; + + ResetTask(DegradeRule rule) { + this.rule = rule; + } + + @Override + public void run() { + rule.getPassCount().set(0); + rule.setCut(false); + } + } +} + diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java new file mode 100755 index 00000000..fbc43fff --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRuleManager.java @@ -0,0 +1,163 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.degrade; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.StringUtil; + +/*** + * @author youji.zj + * @author jialiang.linjl + */ +public class DegradeRuleManager { + + private static volatile Map> degradeRules + = new ConcurrentHashMap>(); + + final static RulePropertyListener listener = new RulePropertyListener(); + private static SentinelProperty> currentProperty + = new DynamicSentinelProperty>(); + + static { + currentProperty.addListener(listener); + } + + /** + * Listen to the {@link SentinelProperty} for {@link DegradeRule}s. The property is the source + * of {@link DegradeRule}s. Degrade rules can also be set by {@link #loadRules(List)} directly. + * + * @param property the property to listen. + */ + public static void register2Property(SentinelProperty> property) { + synchronized (listener) { + currentProperty.removeListener(listener); + property.addListener(listener); + currentProperty = property; + } + } + + public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count) + throws BlockException { + if (degradeRules == null) { + return; + } + + List rules = degradeRules.get(resource.getName()); + if (rules == null) { + return; + } + + for (DegradeRule rule : rules) { + if (!rule.passCheck(context, node, count)) { + throw new DegradeException(rule.getLimitApp()); + } + } + } + + public static boolean hasConfig(String resource) { + return degradeRules.containsKey(resource); + } + + /** + * Get a copy of the rules. + * + * @return a new copy of the rules. + */ + public static List getRules() { + List rules = new ArrayList(); + if (degradeRules == null) { + return rules; + } + for (Map.Entry> entry : degradeRules.entrySet()) { + rules.addAll(entry.getValue()); + } + return rules; + } + + /** + * Load {@link DegradeRule}s, former rules will be replaced. + * + * @param rules new rules to load. + */ + public static void loadRules(List rules) { + try { + currentProperty.updateValue(rules); + } catch (Throwable e) { + RecordLog.info(e.getMessage(), e); + } + } + + private static class RulePropertyListener implements PropertyListener> { + + @Override + public void configUpdate(List conf) { + Map> rules = loadDegradeConf(conf); + if (rules != null) { + degradeRules.clear(); + degradeRules.putAll(rules); + } + RecordLog.info("receive degrade config: " + degradeRules); + } + + @Override + public void configLoad(List conf) { + Map> rules = loadDegradeConf(conf); + if (rules != null) { + degradeRules.clear(); + degradeRules.putAll(rules); + } + RecordLog.info("init degrade config: " + degradeRules); + } + + private Map> loadDegradeConf(List list) { + if (list == null) { + return null; + } + Map> newRuleMap = new ConcurrentHashMap>(); + + for (DegradeRule rule : list) { + if (StringUtil.isBlank(rule.getLimitApp())) { + rule.setLimitApp(FlowRule.DEFAULT); + } + + String identity = rule.getResource(); + List ruleM = newRuleMap.get(identity); + if (ruleM == null) { + ruleM = new ArrayList(); + newRuleMap.put(identity, ruleM); + } + ruleM.add(rule); + } + + return newRuleMap; + } + + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeSlot.java new file mode 100755 index 00000000..533a9350 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeSlot.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.degrade; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * A {@link ProcessorSlot} dedicates to {@link DegradeRule} checking. + * + * @author leyou + */ +public class DegradeSlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count); + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/ColdFactorProperty.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/ColdFactorProperty.java new file mode 100755 index 00000000..679d86af --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/ColdFactorProperty.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author jialiang.linjl + */ +class ColdFactorProperty { + public static volatile int coldFactor = 3; + + static { + String strConfig = SentinelConfig.getConfig(SentinelConfig.COLD_FACTOR); + if (StringUtil.isBlank(strConfig)) { + coldFactor = 3; + } else { + try { + coldFactor = Integer.valueOf(strConfig); + } catch (NumberFormatException e) { + RecordLog.info(e.getMessage(), e); + } + + if (coldFactor <= 1) { + coldFactor = 3; + RecordLog.info("cold factor should be larger than 3"); + } + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/Controller.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/Controller.java new file mode 100755 index 00000000..ab58e751 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/Controller.java @@ -0,0 +1,27 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import com.alibaba.csp.sentinel.node.Node; + +/** + * @author jialiang.linjl + */ +public interface Controller { + + boolean canPass(Node node, int acquireCount); + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowException.java new file mode 100755 index 00000000..81c2a0e3 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowException.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * @author youji.zj + */ +public class FlowException extends BlockException { + + public FlowException(String ruleLimitApp) { + + super(ruleLimitApp); + } + + public FlowException(String message, Throwable cause) { + super(message, cause); + } + + public FlowException(String ruleLimitApp, String message) { + super(ruleLimitApp, message); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRule.java new file mode 100755 index 00000000..87977251 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRule.java @@ -0,0 +1,297 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/*** + *

+ * Each flow rule is mainly composed of three factors: grade, + * strategy and controlBehavior. + *

+ *
    + *
  • The {@link #grade} represents the threshold type of flow control (by QPS or thread count).
  • + *
  • The {@link #strategy} represents the strategy based on invocation relation.
  • + *
  • The {@link #controlBehavior} represents the QPS shaping behavior (actions on incoming request when QPS + * exceeds the threshold).
  • + *
+ * + * @author jialiang.linjl + * @author Eric Zhao + */ +public class FlowRule extends AbstractRule { + + public static final String DEFAULT = "default"; + public static final String OTHER = "other"; + + /** + * The threshold type of flow control (0: thread count, 1: QPS). + */ + private int grade; + + private double count; + + /** + * 0为直接限流;1为关联限流;2为链路限流 + */ + private int strategy; + + private String refResource; + + /** + * Rate limiter control behavior. + * 0. default, 1. warm up, 2. rate limiter + */ + private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT; + + private int warmUpPeriodSec = 10; + + /** + * Max queueing time in rate limiter behavior. + */ + private int maxQueueingTimeMs = 500; + + private Controller controller; + + public int getControlBehavior() { + return controlBehavior; + } + + public FlowRule setControlBehavior(int controlBehavior) { + this.controlBehavior = controlBehavior; + return this; + } + + public int getMaxQueueingTimeMs() { + return maxQueueingTimeMs; + } + + public FlowRule setMaxQueueingTimeMs(int maxQueueingTimeMs) { + this.maxQueueingTimeMs = maxQueueingTimeMs; + return this; + } + + public FlowRule setRater(Controller rater) { + this.controller = rater; + return this; + } + + public int getWarmUpPeriodSec() { + return warmUpPeriodSec; + } + + public FlowRule setWarmUpPeriodSec(int warmUpPeriodSec) { + this.warmUpPeriodSec = warmUpPeriodSec; + return this; + } + + public int getGrade() { + return grade; + } + + public FlowRule setGrade(int grade) { + this.grade = grade; + return this; + } + + public double getCount() { + return count; + } + + public FlowRule setCount(double count) { + this.count = count; + return this; + } + + public int getStrategy() { + return strategy; + } + + public FlowRule setStrategy(int strategy) { + this.strategy = strategy; + return this; + } + + public String getRefResource() { + return refResource; + } + + public FlowRule setRefResource(String refResource) { + this.refResource = refResource; + return this; + } + + @Override + public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) { + String limitApp = this.getLimitApp(); + if (limitApp == null) { + return true; + } + + String origin = context.getOrigin(); + Node selectedNode = selectNodeByRequesterAndStrategy(origin, context, node); + if (selectedNode == null) { + return true; + } + + return controller.canPass(selectedNode, acquireCount); + } + + private Node selectNodeByRequesterAndStrategy(String origin, Context context, DefaultNode node) { + // The limit app should not be empty. + String limitApp = this.getLimitApp(); + + if (limitApp.equals(origin)) { + if (strategy == RuleConstant.STRATEGY_DIRECT) { + return context.getOriginNode(); + } + + String refResource = this.getRefResource(); + if (StringUtil.isEmpty(refResource)) { + return null; + } + + if (strategy == RuleConstant.STRATEGY_RELATE) { + return ClusterBuilderSlot.getClusterNode(refResource); + } + + if (strategy == RuleConstant.STRATEGY_CHAIN) { + if (!refResource.equals(context.getName())) { + return null; + } + return node; + } + + } else if (limitApp.equals(DEFAULT)) { + if (strategy == RuleConstant.STRATEGY_DIRECT) { + return node.getClusterNode(); + } + String refResource = this.getRefResource(); + if (StringUtil.isEmpty(refResource)) { + return null; + } + + if (strategy == RuleConstant.STRATEGY_RELATE) { + return ClusterBuilderSlot.getClusterNode(refResource); + } + + if (strategy == RuleConstant.STRATEGY_CHAIN) { + if (!refResource.equals(context.getName())) { + return null; + } + return node; + } + + } else if (limitApp.equals(OTHER) && FlowRuleManager.isOtherOrigin(origin, getResource())) { + if (strategy == RuleConstant.STRATEGY_DIRECT) { + return context.getOriginNode(); + } + + String refResource = this.getRefResource(); + if (StringUtil.isEmpty(refResource)) { + return null; + } + if (strategy == RuleConstant.STRATEGY_RELATE) { + return ClusterBuilderSlot.getClusterNode(refResource); + } + + if (strategy == RuleConstant.STRATEGY_CHAIN) { + if (!refResource.equals(context.getName())) { + return null; + } + if (node != null) { + return node; + } + } + } + + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FlowRule)) { + return false; + } + if (!super.equals(o)) { + return false; + } + + FlowRule flowRule = (FlowRule)o; + + if (grade != flowRule.grade) { + return false; + } + if (Double.compare(flowRule.count, count) != 0) { + return false; + } + if (strategy != flowRule.strategy) { + return false; + } + if (refResource != null ? !refResource.equals(flowRule.refResource) : flowRule.refResource != null) { + return false; + } + if (this.controlBehavior != flowRule.controlBehavior) { + return false; + } + + if (warmUpPeriodSec != flowRule.warmUpPeriodSec) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + result = 31 * result + grade; + temp = Double.doubleToLongBits(count); + result = 31 * result + (int)(temp ^ (temp >>> 32)); + result = 31 * result + strategy; + result = 31 * result + (refResource != null ? refResource.hashCode() : 0); + result = 31 * result + (int)(temp ^ (temp >>> 32)); + result = 31 * result + warmUpPeriodSec; + result = 31 * result + controlBehavior; + return result; + } + + @Override + public String toString() { + return "FlowRule{" + + "resource=" + getResource() + + ", limitApp=" + getLimitApp() + + ", grade=" + grade + + ", count=" + count + + ", strategy=" + strategy + + ", refResource=" + refResource + + ", controlBehavior=" + controlBehavior + + ", warmUpPeriodSec=" + warmUpPeriodSec + + ", maxQueueingTimeMs=" + maxQueueingTimeMs + + ", controller=" + controller + + "}"; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleComparator.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleComparator.java new file mode 100755 index 00000000..2af7378a --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleComparator.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import java.util.Comparator; + +public class FlowRuleComparator implements Comparator { + + @Override + public int compare(FlowRule o1, FlowRule o2) { + + if (o1.getLimitApp() == null) { + return 0; + } + + if (o1.getLimitApp().equals(o2.getLimitApp())) { + return 0; + } + + if (FlowRule.DEFAULT.equals(o1.getLimitApp())) { + return 1; + } else if (FlowRule.DEFAULT.equals(o2.getLimitApp())) { + return -1; + } else { + return 0; + } + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleManager.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleManager.java new file mode 100755 index 00000000..b734fa91 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleManager.java @@ -0,0 +1,206 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.metric.MetricTimerListener; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController; +import com.alibaba.csp.sentinel.slots.block.flow.controller.PaceController; +import com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController; + +/** + *

+ * One resources can have multiple rules. And these rules take effects in the + * following order: + *

    + *
  1. requests from specified caller
  2. + *
  3. no specified caller
  4. + *
+ *

+ * + * @author jialiang.linjl + */ + +public class FlowRuleManager { + + private static final Map> flowRules = new ConcurrentHashMap>(); + private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final static FlowPropertyListener listener = new FlowPropertyListener(); + private static SentinelProperty> currentProperty = new DynamicSentinelProperty>(); + + static { + currentProperty.addListener(listener); + scheduler.scheduleAtFixedRate(new MetricTimerListener(), 0, 1, TimeUnit.SECONDS); + } + + /** + * Listen to the {@link SentinelProperty} for {@link FlowRule}s. The property is the source of {@link FlowRule}s. + * Flow rules can also be set by {@link #loadRules(List)} directly. + * + * @param property the property to listen. + */ + public static void register2Property(SentinelProperty> property) { + synchronized (listener) { + currentProperty.removeListener(listener); + property.addListener(listener); + currentProperty = property; + } + } + + /** + * Get a copy of the rules. + * + * @return a new copy of the rules. + */ + public static List getRules() { + List rules = new ArrayList(); + if (flowRules == null) { + return rules; + } + for (Map.Entry> entry : flowRules.entrySet()) { + rules.addAll(entry.getValue()); + } + return rules; + } + + /** + * Load {@link FlowRule}s, former rules will be replaced. + * + * @param rules new rules to load. + */ + public static void loadRules(List rules) { + currentProperty.updateValue(rules); + } + + private static Map> loadFlowConf(List list) { + Map> newRuleMap = new ConcurrentHashMap>(); + + if (list == null) { + return newRuleMap; + } + + for (FlowRule rule : list) { + if (StringUtil.isBlank(rule.getLimitApp())) { + rule.setLimitApp(FlowRule.DEFAULT); + } + + Controller rater = new DefaultController(rule.getCount(), rule.getGrade()); + if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS + && rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_WARM_UP + && rule.getWarmUpPeriodSec() > 0) { + rater = new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(), ColdFactorProperty.coldFactor); + + } else if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS + && rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER + && rule.getMaxQueueingTimeMs() > 0) { + rater = new PaceController(rule.getMaxQueueingTimeMs(), rule.getCount()); + } + rule.setRater(rater); + + String identity = rule.getResource(); + List ruleM = newRuleMap.get(identity); + + if (ruleM == null) { + ruleM = new ArrayList(); + newRuleMap.put(identity, ruleM); + } + + ruleM.add(rule); + + } + return newRuleMap; + } + + public static void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count) + throws BlockException { + if (flowRules != null) { + List rules = flowRules.get(resource.getName()); + if (rules != null) { + for (FlowRule rule : rules) { + if (!rule.passCheck(context, node, count)) { + throw new FlowException(rule.getLimitApp()); + } + } + } + } + } + + public static boolean hasConfig(String resource) { + return flowRules.containsKey(resource); + } + + public static boolean isOtherOrigin(String origin, String resourceName) { + if (StringUtil.isEmpty(origin)) { + return false; + } + + if (flowRules != null) { + List rules = flowRules.get(resourceName); + + if (rules != null) { + for (FlowRule rule : rules) { + if (origin.equals(rule.getLimitApp())) { + return false; + } + } + } + } + + return true; + } + + private static final class FlowPropertyListener implements PropertyListener> { + + @Override + public void configUpdate(List value) { + Map> rules = loadFlowConf(value); + if (rules != null) { + flowRules.clear(); + flowRules.putAll(rules); + } + RecordLog.info("receive flow config: " + flowRules); + } + + @Override + public void configLoad(List conf) { + Map> rules = loadFlowConf(conf); + if (rules != null) { + flowRules.clear(); + flowRules.putAll(rules); + } + RecordLog.info("load flow config: " + flowRules); + } + + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowSlot.java new file mode 100755 index 00000000..d01856df --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowSlot.java @@ -0,0 +1,127 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + *

+ * Combined the runtime statistics collected from the previous + * slots(NodeSelectorSlot, ClusterNodeBuilderSlot, and StatistcSlot), FlowSlot + * will use pre-set rules to decide whether the incoming requests should be + * blocked. + * + * {@code SphU.entry (resourceName) }will throw FlowException if any rule is + * triggered. user can customize his own logic by catching FlowException. + * + * One resource can have multiple flow rules. FlowSlot traverses these rules + * until one of them is triggered or all rules have been traversed. + * + * Each FlowRule is mainly composed of the 2 factors: grade, strategy, path; we + * can combine these factors to achieve different effects. + * + * The grade is defined by the grade field in FlowRule. Here, 0 for thread + * isolation and 1 for request count shaping. Both thread count and request + * count are collected in real runtime, and we can view these statistics by + * following command: {@code + * curl http:// localhost:8719 / tree?type = root` + * idx id thread pass blocked success total aRt 1m-pass 1m-block 1m-all exeption + * 2 abc647 0 460 46 46 1 27 630 276 897 0 + * } + * + * Thread for the count of threads that is currently processing the resource; + * pass for the count of incoming request within one second; blocked for the + * count of requests blocked within one second; success for the count of the + * requests successfully within one second; RT for the average response time of + * the requests within a second; total for the sum of incoming requests and + * blocked requests within one second; 1m-pass is for the count of incoming + * requests within one minute; 1m-block is for the count of a request blocked + * within one minute; 1m -all is the total of incoming and blocked requests + * within 1 minute; exception is for the count of exceptions in one second. + * + * This stage is usually used to protect resources from occupying. If a resource + * takes long time to finish, threads will begin to occupy. The longer the + * response takes, the more threads occupy. + * + * Besides counter, thread pool or semaphore can also be used to achieve this. + * + * - Thread pool: Allocate a thread pool to handle these resource. When there is + * no more idle thread in the pool, the request is rejected without affecting + * other resources. + * + * - Semaphore: Use semaphore to control the concurrent count of the threads in + * this resource. + * + * The benefit of using thread pool is that, it can walk away gracefully when + * time out. But it also bring us the cost of context switch and additional + * threads. If the incoming requests is already served in a separated thread, + * for instance, a servelet request, it will almost double the threads count if + * using thread pool. + * + * ### QPS Shaping ### When qps exceeds the threshold, we will take actions to + * control the incoming request, and is configured by "controlBehavior" field in + * flowrule + * + * 1. immediately reject(RuleConstant.CONTROL_BEHAVIOR_DEFAULT) + * + * This is the default behavior. The exceeded request is rejected immediately + * and the FlowException is thrown + * + * 2. Warmup(RuleConstant.CONTROL_BEHAVIOR_WARM_UP) + * + * If the usage of system has been low for a while, and a large amount of + * requests comes, the system might not be able to handle all these requests at + * once. However if we steady increase the incoming request, the system can warm + * up and finally be able to handle all the requests.If the usage of system has + * been low for a while, and a large amount of requests comes, the system might + * not be able to handle all these requests at once. However if we steady + * increase the incoming request, the system can warm up and finally be able to + * handle all the requests. This warmup period can be configured by setting the + * field "warmUpPeriodSec" in flow rule. + * + * 3.Rate limiter(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) This strategy + * strictly controls the interval between requests. In other words, it allows + * requests to pass at a stable rate. + * This strategy is an implement of leaky bucket + * (https://en.wikipedia.org/wiki/Leaky_bucket). It is used to handle the + * request at a stable rate and is often used in burst traffic. For instance, + * Message. When a large number of requests beyond the system’s capacity arrive + * at the same time, the system using this strategy will handle requests and its + * fixed rate until all the requests have been processed or time out. + * + * @author jialiang.linjl + */ +public class FlowSlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + + FlowRuleManager.checkFlow(resourceWrapper, context, node, count); + + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/DefaultController.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/DefaultController.java new file mode 100755 index 00000000..d02d85a8 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/DefaultController.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow.controller; + +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.Controller; + +/** + * @author jialiang.linjl + */ +public class DefaultController implements Controller { + + double count = 0; + int grade = 0; + + public DefaultController(double count, int grade) { + super(); + this.count = count; + this.grade = grade; + } + + @Override + public boolean canPass(Node node, int acquireCount) { + int curCount = avgUsedTokens(node); + if (curCount + acquireCount > count) { + return false; + } + + return true; + } + + private int avgUsedTokens(Node node) { + if (node == null) { + return -1; + } + return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)node.passQps(); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/PaceController.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/PaceController.java new file mode 100755 index 00000000..da56d439 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/PaceController.java @@ -0,0 +1,77 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow.controller; + +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.csp.sentinel.slots.block.flow.Controller; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.node.Node; + +/** + * @author jialiang.linjl + */ +public class PaceController implements Controller { + + private final int maxQueueingTimeMs; + private final double count; + private final AtomicLong latestPassedTime = new AtomicLong(-1); + + public PaceController(int timeOut, double count) { + this.maxQueueingTimeMs = timeOut; + this.count = count; + } + + @Override + public boolean canPass(Node node, int acquireCount) { + + // 按照斜率来计算计划中应该什么时候通过 + long currentTime = TimeUtil.currentTimeMillis(); + + long costTime = Math.round(1.0 * (acquireCount) / count * 1000); + + //期待时间 + long expectedTime = costTime + latestPassedTime.get(); + + if (expectedTime <= currentTime) { + //这里会有冲突,然而冲突就冲突吧. + latestPassedTime.set(currentTime); + return true; + } else { + // 计算自己需要的等待时间 + long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis(); + if (waitTime >= maxQueueingTimeMs) { + return false; + } else { + long oldTime = latestPassedTime.addAndGet(costTime); + try { + waitTime = oldTime - TimeUtil.currentTimeMillis(); + if (waitTime >= maxQueueingTimeMs) { + latestPassedTime.addAndGet(-costTime); + return false; + } + Thread.sleep(waitTime); + return true; + } catch (InterruptedException e) { + } + } + } + + return false; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/WarmUpController.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/WarmUpController.java new file mode 100755 index 00000000..9037d1e9 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/controller/WarmUpController.java @@ -0,0 +1,172 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow.controller; + +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.flow.Controller; + +/** + * The principle idea comes from guava. However, the calculation of guava is + * rate-based, which means that we need to translate rate to qps. + * + * https://github.com/google/guava/blob/master/guava/src/com/google/common/util/concurrent/SmoothRateLimiter.java + * + * Requests arriving at the pulse may drag down long idle systems even though it + * has a much larger handling capability in stable period. It usually happens in + * scenarios that require extra time for initialization, for example, db + * establishes a connection; connects to a remote service, and so on. + * + * That’s why we need “warm up”. + * + * Sentinel’s “warm up” implementation is based on Guava's algorithm. However, + * unlike Guava's scenario, which is a “leaky bucket”, and is mainly used to + * adjust the request interval, Sentinel is more focus on controlling the count + * of incoming requests per second without calculating its interval. + * + * Sentinel's "warm-up" implementation is based on the guava-based algorithm. + * However, Guava’s implementation focus on adjusting the request interval, in + * other words, a Leaky bucket. Sentinel pays more attention to controlling the + * count of incoming requests per second without calculating its interval, it is + * more like a “Token bucket.” + * + * + * The remaining tokens in the bucket is used to measure the system utility. + * Suppose a system can handle b requests per second. Every second b tokens will + * be added into the bucket until the bucket is full. And when system processes + * a request, it takes a token from the bucket. The more tokens left in the + * bucket, the lower the utilization of the system; when the token in the token + * bucket is above a certain threshold, we call it in a "saturation" state. + * + * Base on Guava’s theory, there is a linear equation we can write this in the + * form y = m * x + b where y (a.k.a y(x)), or qps(q)), is our expected QPS + * given a saturated period (e.g. 3 minutes in), m is the rate of change from + * our cold (minimum) rate to our stable (maximum) rate, x (or q) is the + * occupied token. + * + * @author jialiang.linjl + */ +public class WarmUpController implements Controller { + + private double count; + private int coldFactor; + private int warningToken = 0; + private int maxToken; + private double slope; + + AtomicLong storedTokens = new AtomicLong(0); + AtomicLong lastFilledTime = new AtomicLong(0); + + public WarmUpController(double count, int warmupPeriodInSec, int coldFactor) { + construct(count, warmupPeriodInSec, coldFactor); + } + + public WarmUpController(double count, int warmUpPeriodInMic) { + construct(count, warmUpPeriodInMic, 3); + } + + private void construct(double count, int warmUpPeriodInSec, int coldFactor) { + + if (coldFactor <= 1) { + throw new RuntimeException("cold factor should be larget than 1"); + } + + this.count = count; + + this.coldFactor = coldFactor; + + // thresholdPermits = 0.5 * warmupPeriod / stableInterval. + // warningToken = 100; + warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1); + // / maxPermits = thresholdPermits + 2 * warmupPeriod / + // (stableInterval + coldInterval) + // maxToken = 200 + maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor)); + + // slope + // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits + // - thresholdPermits); + slope = (coldFactor - 1.0) / count / (maxToken - warningToken); + + } + + @Override + public boolean canPass(Node node, int acquireCount) { + long passQps = node.passQps(); + + long previousQps = node.previousPassQps(); + syncToken(previousQps); + + // 开始计算它的斜率 + // 如果进入了警戒线,开始调整他的qps + long restToken = storedTokens.get(); + if (restToken >= warningToken) { + long aboveToken = restToken - warningToken; + // 消耗的速度要比warning快,但是要比慢 + // current interval = restToken*slope+1/count + double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count)); + if (passQps + acquireCount <= warningQps) { + return true; + } + } else { + if (passQps + acquireCount <= count) { + return true; + } + } + + return false; + } + + private void syncToken(long passQps) { + long currentTime = TimeUtil.currentTimeMillis(); + currentTime = currentTime - currentTime % 1000; + long oldLastFillTime = lastFilledTime.get(); + if (currentTime <= oldLastFillTime) { + return; + } + + long oldValue = storedTokens.get(); + long newValue = coolDownTokens(currentTime, passQps); + + if (storedTokens.compareAndSet(oldValue, newValue)) { + long currentValue = storedTokens.addAndGet(0 - passQps); + if (currentValue < 0) { + storedTokens.set(0l); + } + lastFilledTime.set(currentTime); + } + + } + + private long coolDownTokens(long currentTime, long passQps) { + long oldValue = storedTokens.get(); + long newValue = oldValue; + + // 添加令牌的判断前提条件: + // 当令牌的消耗程度远远低于警戒线的时候 + if (oldValue < warningToken) { + newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000); + } else if (oldValue > warningToken) { + if (passQps < (int)count / coldFactor) { + newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000); + } + } + return Math.min(newValue, maxToken); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java new file mode 100755 index 00000000..92940b46 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterBuilderSlot.java @@ -0,0 +1,153 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.clusterbuilder; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.Env; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlotChain; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; + +/** + *

+ * This slot maintains resource running statistics (response time, qps, thread + * count, exception), and a list of callers as well which is marked by + * {@link ContextUtil#enter(String origin)} + *

+ *

+ * One resource has only one cluster node, while one resource can have multiple + * default node. + *

+ * + * @author jialiang.linjl + */ +public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot { + + /** + *

+ * Remember that same resource({@link ResourceWrapper#equals(Object)}) will share + * the same {@link ProcessorSlotChain} globally, no matter in witch context. So if + * code goes into {@link #entry(Context, ResourceWrapper, DefaultNode, int, Object...)}, + * the resource name must be same but context name may not. + *

+ *

+ * To get total statistics of the same resource in different context, same resource + * shares the same {@link ClusterNode} globally. All {@link ClusterNode}s are cached + * in this map. + *

+ *

+ * The longer the application runs, the more stable this mapping will + * become. so we don't concurrent map but a lock. as this lock only happens + * at the very beginning while concurrent map will hold the lock all the + * time + *

+ */ + private static volatile Map clusterNodeMap + = new HashMap(); + + private static final Object lock = new Object(); + + private ClusterNode clusterNode = null; + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + if (clusterNode == null) { + synchronized (lock) { + if (clusterNode == null) { + // Create the cluster node. + clusterNode = Env.nodeBuilder.buildClusterNode(); + HashMap newMap = new HashMap(16); + newMap.putAll(clusterNodeMap); + newMap.put(node.getId(), clusterNode); + + clusterNodeMap = newMap; + } + } + } + node.setClusterNode(clusterNode); + + /* + * if context origin is set, we should get or create a new {@link Node} of + * the specific origin. + */ + if (!"".equals(context.getOrigin())) { + Node originNode = node.getClusterNode().getOriginNode(context.getOrigin()); + context.getCurEntry().setOriginNode(originNode); + } + + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } + + /** + * Get {@link ClusterNode} of the resource of the specific type. + * + * @param id resource name. + * @param type invoke type. + * @return the {@link ClusterNode} + */ + public static ClusterNode getClusterNode(String id, EntryType type) { + return clusterNodeMap.get(new StringResourceWrapper(id, type)); + } + + /** + * Get {@link ClusterNode} of the resource name. + * + * @param id resource name. + * @return the {@link ClusterNode}. + */ + public static ClusterNode getClusterNode(String id) { + if (id == null) { + return null; + } + ClusterNode clusterNode = null; + + for (EntryType nodeType : EntryType.values()) { + clusterNode = clusterNodeMap.get(new StringResourceWrapper(id, nodeType)); + if (clusterNode != null) { + break; + } + } + + return clusterNode; + } + + /** + * Get {@link ClusterNode}s map, this map holds all {@link ClusterNode}s, it's key is resource name, + * value is the related {@link ClusterNode}.
+ * DO NOT MODIFY the map returned. + * + * @return all {@link ClusterNode}s + */ + public static Map getClusterNodeMap() { + return clusterNodeMap; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtil.java new file mode 100755 index 00000000..7cca5c17 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/EagleEyeLogUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.logger; + +import java.io.File; + +import com.alibaba.csp.sentinel.eagleeye.EagleEye; +import com.alibaba.csp.sentinel.eagleeye.StatLogger; + +public class EagleEyeLogUtil { + + private static final String DIR_NAME = "csp"; + private static final String FILE_NAME = "sentinel-block.log"; + + private static StatLogger statLogger; + + static { + String path = DIR_NAME + File.separator + FILE_NAME; + + statLogger = EagleEye.statLoggerBuilder("sentinel-block-record") + .intervalSeconds(1) + .entryDelimiter('|') + .keyDelimiter(',') + .valueDelimiter(',') + .maxEntryCount(6000) + .baseLogFilePath(path) + .maxFileSizeMB(300) + .maxBackupIndex(3) + .buildSingleton(); + } + + public static void log(String resource, String exceptionName, String ruleLimitApp, String origin, int count) { + statLogger.stat(resource, exceptionName, ruleLimitApp, origin).count(count); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/LogSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/LogSlot.java new file mode 100755 index 00000000..e2bdff32 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/logger/LogSlot.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.logger; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * A {@link com.alibaba.csp.sentinel.slotchain.ProcessorSlot} that is response for logging block exceptions + * to provide concrete logs for troubleshooting. + */ +public class LogSlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, Object... args) + throws Throwable { + try { + fireEntry(context, resourceWrapper, obj, count, args); + } catch (BlockException e) { + EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(), + context.getOrigin(), count); + throw e; + } catch (Throwable e) { + RecordLog.info("Entry exception", e); + } + + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + try { + fireExit(context, resourceWrapper, count, args); + } catch (Throwable e) { + RecordLog.info("Entry exit exception", e); + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorSlot.java new file mode 100755 index 00000000..144780e6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorSlot.java @@ -0,0 +1,178 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.nodeselector; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.Env; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + *

+ * This class will try to build the calling traces via + *
    + *
  1. adding a new {@link DefaultNode} if needed as the last child in the context. + * The context's last node is the current node or the parent node of the context.
  2. + *
  3. setting itself to the the context current node.
  4. + *
+ *

+ * + *

It works as follow:

+ *
+ * ContextUtil.enter("entrance1", "appA");
+ * Entry nodeA = SphU.entry("nodeA");
+ * if (nodeA != null) {
+ *     nodeA.exit();
+ * }
+ * ContextUtil.exit();
+ * 
+ * + * Above code will generate the following invocation structure in memory: + * + *
+ *
+ *              machine-root
+ *                  /
+ *                 /
+ *           EntranceNode1
+ *               /
+ *              /
+ *        DefaultNode(nodeA)- - - - - -> ClusterNode(nodeA);
+ * 
+ * + *

+ * Here the {@link EntranceNode} represents "entrance1" given by + * {@code ContextUtil.enter("entrance1", "appA")}. + *

+ *

+ * Both DefaultNode(nodeA) and ClusterNode(nodeA) holds statistics of "nodeA", which is given + * by {@code SphU.entry("nodeA")} + *

+ *

+ * The {@link ClusterNode} is uniquely identified by the ResourceId; the {@link DefaultNode} + * is identified by both the resource id and {@link Context}. In other words, one resource + * id will generate multiple {@link DefaultNode} for each distinct context, but only one + * {@link ClusterNode}. + *

+ *

+ * the following code shows one resource id in two different context: + *

+ * + *
+ *    ContextUtil.enter("entrance1", "appA");
+ *    Entry nodeA = SphU.entry("nodeA");
+ *    if (nodeA != null) {
+ *        nodeA.exit();
+ *    }
+ *    ContextUtil.exit();
+ *
+ *    ContextUtil.enter("entrance2", "appA");
+ *    nodeA = SphU.entry("nodeA");
+ *    if (nodeA != null) {
+ *        nodeA.exit();
+ *    }
+ *    ContextUtil.exit();
+ * 
+ * + * Above code will generate the following invocation structure in memory: + * + *
+ *
+ *                  machine-root
+ *                  /         \
+ *                 /           \
+ *         EntranceNode1   EntranceNode2
+ *               /               \
+ *              /                 \
+ *      DefaultNode(nodeA)   DefaultNode(nodeA)
+ *             |                    |
+ *             +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
+ * 
+ * + *

+ * As we can see, two {@link DefaultNode} are created for "nodeA" in two context, but only one + * {@link ClusterNode} is created. + *

+ * + *

+ * We can also check this structure by calling:
+ * {@code curl http://localhost:8719/tree?type=root} + *

+ * + * @author jialiang.linjl + * @see EntranceNode + * @see ContextUtil + */ +public class NodeSelectorSlot extends AbstractLinkedProcessorSlot { + + /** + * {@link DefaultNode}s of the same resource in different context. + */ + private Map map = new HashMap(10); + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) + throws Throwable { + /* + * It's interesting that we use context name rather resource name as the map key. + * + * Remember that same resource({@link ResourceWrapper#equals(Object)}) will share + * the same {@link ProcessorSlotChain} globally, no matter in which context. So if + * code goes into {@link #entry(Context, ResourceWrapper, DefaultNode, int, Object...)}, + * the resource name must be same but context name may not. + * + * If we use {@link com.alibaba.csp.sentinel.SphU#entry(String resource)} to + * enter same resource in different context, using context name as map key can + * distinguish the same resource. In this case, multiple {@link DefaultNode}s will be created + * of the same resource name, for every distinct context (different context name) each. + * + * Consider another question. One resource may have multiple {@link DefaultNode}, + * so what is the fastest way to get total statistics of the same resource? + * The answer is all {@link DefaultNode}s with same resource name share one + * {@link ClusterNode}. See {@link ClusterBuilderSlot} for detail. + */ + DefaultNode node = map.get(context.getName()); + if (node == null) { + synchronized (this) { + node = map.get(context.getName()); + if (node == null) { + node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null); + HashMap cacheMap = new HashMap(map.size()); + cacheMap.putAll(map); + cacheMap.put(context.getName(), node); + map = cacheMap; + } + // Build invocation tree + ((DefaultNode)context.getLastNode()).addChild(node); + } + } + + context.setCurNode(node); + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java new file mode 100755 index 00000000..c4e4d8f6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/StatisticSlot.java @@ -0,0 +1,127 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + *

+ * A processor slot that dedicates to real time statistics. + * When entering this slot, we need to separately count the following + * information: + *

    + *
  • {@link ClusterNode}: total statistics of a cluster node of the resource id  
  • + *
  • origin node: statistics of a cluster node from different callers/origins.
  • + *
  • {@link DefaultNode}: statistics for specific resource name in the specific context. + *
  • Finally, the sum statistics of all entrances.
  • + *
+ *

+ * + * @author jialiang.linjl + */ +public class StatisticSlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + + try { + fireEntry(context, resourceWrapper, node, count, args); + node.increaseThreadNum(); + node.addPassRequest(); + + if (context.getCurEntry().getOriginNode() != null) { + context.getCurEntry().getOriginNode().increaseThreadNum(); + context.getCurEntry().getOriginNode().addPassRequest(); + } + + if (resourceWrapper.getType() == EntryType.IN) { + Constants.ENTRY_NODE.increaseThreadNum(); + Constants.ENTRY_NODE.addPassRequest(); + } + + } catch (BlockException e) { + context.getCurEntry().setError(e); + + // Add block count. + node.increaseBlockedQps(); + if (context.getCurEntry().getOriginNode() != null) { + context.getCurEntry().getOriginNode().increaseBlockedQps(); + } + + if (resourceWrapper.getType() == EntryType.IN) { + Constants.ENTRY_NODE.increaseBlockedQps(); + } + + throw e; + } catch (Throwable e) { + context.getCurEntry().setError(e); + + // Should not happen + node.increaseExceptionQps(); + if (context.getCurEntry().getOriginNode() != null) { + context.getCurEntry().getOriginNode().increaseExceptionQps(); + } + + if (resourceWrapper.getType() == EntryType.IN) { + Constants.ENTRY_NODE.increaseExceptionQps(); + } + throw e; + } + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + DefaultNode node = (DefaultNode)context.getCurNode(); + + if (context.getCurEntry().getError() == null) { + long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime(); + if (rt > Constants.TIME_DROP_VALVE) { + rt = Constants.TIME_DROP_VALVE; + } + + node.rt(rt); + if (context.getCurEntry().getOriginNode() != null) { + context.getCurEntry().getOriginNode().rt(rt); + } + + node.decreaseThreadNum(); + + if (context.getCurEntry().getOriginNode() != null) { + context.getCurEntry().getOriginNode().decreaseThreadNum(); + } + + if (resourceWrapper.getType() == EntryType.IN) { + Constants.ENTRY_NODE.rt(rt); + Constants.ENTRY_NODE.decreaseThreadNum(); + } + } else { + // error may happen + // node.rt(-2); + } + + fireExit(context, resourceWrapper, count); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java new file mode 100755 index 00000000..78f5d627 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java @@ -0,0 +1,113 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic.base; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import com.alibaba.csp.sentinel.util.TimeUtil; + +/** + * @param type of data wrapper + * @author jialiang.linjl + */ +public abstract class LeapArray { + + protected int windowLength; + protected int sampleCount; + protected int intervalInMs; + + protected AtomicReferenceArray> array; + + public LeapArray(int windowLength, int intervalInSec) { + this.windowLength = windowLength; + this.sampleCount = intervalInSec * 1000 / windowLength; + this.intervalInMs = intervalInSec * 1000; + + this.array = new AtomicReferenceArray>(sampleCount); + } + + public WindowWrap currentWindow() { + return currentWindow(TimeUtil.currentTimeMillis()); + } + + abstract public WindowWrap currentWindow(long time); + + public WindowWrap getPreviousWindow(long time) { + long timeId = (time - windowLength) / windowLength; + int idx = (int)(timeId % array.length()); + time = time - windowLength; + WindowWrap wrap = array.get(idx); + + if (wrap == null) { + return wrap; + } + + if (wrap.windowStart() + windowLength < (time)) { + return null; + } + + return wrap; + } + + public WindowWrap getPreviousWindow() { + return getPreviousWindow(System.currentTimeMillis()); + } + + public T getWindowValue(long time) { + long timeId = time / windowLength; + int idx = (int)(timeId % array.length()); + + WindowWrap old = array.get(idx); + if (old == null) { + return null; + } + + return old.value(); + } + + public AtomicReferenceArray> array() { + return array; + } + + public List> list() { + ArrayList> result = new ArrayList>(); + + for (int i = 0; i < array.length(); i++) { + WindowWrap windowWrap = array.get(i); + if (windowWrap == null) { + continue; + } + result.add(windowWrap); + } + + return result; + } + + public List values() { + ArrayList result = new ArrayList(); + + for (int i = 0; i < array.length(); i++) { + WindowWrap windowWrap = array.get(i); + if (windowWrap == null) { + continue; + } + result.add(windowWrap.value()); + } + return result; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java new file mode 100755 index 00000000..beeaedcf --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LongAdder.java @@ -0,0 +1,207 @@ +package com.alibaba.csp.sentinel.slots.statistic.base; + +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * From http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/jsr166e/ + */ + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicLong; + +/** + * One or more variables that together maintain an initially zero + * {@code long} sum. When updates (method {@link #add}) are contended + * across threads, the set of variables may grow dynamically to reduce + * contention. Method {@link #sum} (or, equivalently, {@link + * #longValue}) returns the current total combined across the + * variables maintaining the sum. + * + *

This class is usually preferable to {@link AtomicLong} when + * multiple threads update a common sum that is used for purposes such + * as collecting statistics, not for fine-grained synchronization + * control. Under low update contention, the two classes have similar + * characteristics. But under high contention, expected throughput of + * this class is significantly higher, at the expense of higher space + * consumption. + * + *

This class extends {@link Number}, but does not define + * methods such as {@code hashCode} and {@code compareTo} because + * instances are expected to be mutated, and so are not useful as + * collection keys. + * + *

jsr166e note: This class is targeted to be placed in + * java.util.concurrent.atomic + * + * @author Doug Lea + * @since 1.8 + */ +public class LongAdder extends Striped64 implements Serializable { + private static final long serialVersionUID = 7249069246863182397L; + + /** + * Version of plus for use in retryUpdate + */ + final long fn(long v, long x) { return v + x; } + + /** + * Creates a new adder with initial sum of zero. + */ + public LongAdder() { + } + + /** + * Adds the given value. + * + * @param x the value to add + */ + public void add(long x) { + Cell[] as; + long b, v; + HashCode hc; + Cell a; + int n; + if ((as = cells) != null || !casBase(b = base, b + x)) { + boolean uncontended = true; + int h = (hc = threadHashCode.get()).code; + if (as == null || (n = as.length) < 1 || + (a = as[(n - 1) & h]) == null || + !(uncontended = a.cas(v = a.value, v + x))) { retryUpdate(x, hc, uncontended); } + } + } + + /** + * Equivalent to {@code add(1)}. + */ + public void increment() { + add(1L); + } + + /** + * Equivalent to {@code add(-1)}. + */ + public void decrement() { + add(-1L); + } + + /** + * Returns the current sum. The returned value is NOT an + * atomic snapshot: Invocation in the absence of concurrent + * updates returns an accurate result, but concurrent updates that + * occur while the sum is being calculated might not be + * incorporated. + * + * @return the sum + */ + public long sum() { + long sum = base; + Cell[] as = cells; + if (as != null) { + int n = as.length; + for (int i = 0; i < n; ++i) { + Cell a = as[i]; + if (a != null) { sum += a.value; } + } + } + return sum; + } + + /** + * Resets variables maintaining the sum to zero. This method may + * be a useful alternative to creating a new adder, but is only + * effective if there are no concurrent updates. Because this + * method is intrinsically racy, it should only be used when it is + * known that no threads are concurrently updating. + */ + public void reset() { + internalReset(0L); + } + + /** + * Equivalent in effect to {@link #sum} followed by {@link + * #reset}. This method may apply for example during quiescent + * points between multithreaded computations. If there are + * updates concurrent with this method, the returned value is + * not guaranteed to be the final value occurring before + * the reset. + * + * @return the sum + */ + public long sumThenReset() { + long sum = base; + Cell[] as = cells; + base = 0L; + if (as != null) { + int n = as.length; + for (int i = 0; i < n; ++i) { + Cell a = as[i]; + if (a != null) { + sum += a.value; + a.value = 0L; + } + } + } + return sum; + } + + /** + * Returns the String representation of the {@link #sum}. + * + * @return the String representation of the {@link #sum} + */ + public String toString() { + return Long.toString(sum()); + } + + /** + * Equivalent to {@link #sum}. + * + * @return the sum + */ + public long longValue() { + return sum(); + } + + /** + * Returns the {@link #sum} as an {@code int} after a narrowing + * primitive conversion. + */ + public int intValue() { + return (int)sum(); + } + + /** + * Returns the {@link #sum} as a {@code float} + * after a widening primitive conversion. + */ + public float floatValue() { + return (float)sum(); + } + + /** + * Returns the {@link #sum} as a {@code double} after a widening + * primitive conversion. + */ + public double doubleValue() { + return (double)sum(); + } + + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + s.defaultWriteObject(); + s.writeLong(sum()); + } + + private void readObject(ObjectInputStream s) + throws IOException, ClassNotFoundException { + s.defaultReadObject(); + busy = 0; + cells = null; + base = s.readLong(); + } + +} \ No newline at end of file diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Stripe64.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Stripe64.java new file mode 100755 index 00000000..e4489e8c --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Stripe64.java @@ -0,0 +1,347 @@ + +package com.alibaba.csp.sentinel.slots.statistic.base; + +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * From http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/jsr166e/ + */ + +import java.util.Random; + +/** + * A package-local class holding common representation and mechanics + * for classes supporting dynamic striping on 64bit values. The class + * extends Number so that concrete subclasses must publicly do so. + */ +abstract class Striped64 extends Number { + /* + * This class maintains a lazily-initialized table of atomically + * updated variables, plus an extra "base" field. The table size + * is a power of two. Indexing uses masked per-thread hash codes. + * Nearly all declarations in this class are package-private, + * accessed directly by subclasses. + * + * Table entries are of class Cell; a variant of AtomicLong padded + * to reduce cache contention on most processors. Padding is + * overkill for most Atomics because they are usually irregularly + * scattered in memory and thus don't interfere much with each + * other. But Atomic objects residing in arrays will tend to be + * placed adjacent to each other, and so will most often share + * cache lines (with a huge negative performance impact) without + * this precaution. + * + * In part because Cells are relatively large, we avoid creating + * them until they are needed. When there is no contention, all + * updates are made to the base field. Upon first contention (a + * failed CAS on base update), the table is initialized to size 2. + * The table size is doubled upon further contention until + * reaching the nearest power of two greater than or equal to the + * number of CPUS. Table slots remain empty (null) until they are + * needed. + * + * A single spinlock ("busy") is used for initializing and + * resizing the table, as well as populating slots with new Cells. + * There is no need for a blocking lock: When the lock is not + * available, threads try other slots (or the base). During these + * retries, there is increased contention and reduced locality, + * which is still better than alternatives. + * + * Per-thread hash codes are initialized to random values. + * Contention and/or table collisions are indicated by failed + * CASes when performing an update operation (see method + * retryUpdate). Upon a collision, if the table size is less than + * the capacity, it is doubled in size unless some other thread + * holds the lock. If a hashed slot is empty, and lock is + * available, a new Cell is created. Otherwise, if the slot + * exists, a CAS is tried. Retries proceed by "double hashing", + * using a secondary hash (Marsaglia XorShift) to try to find a + * free slot. + * + * The table size is capped because, when there are more threads + * than CPUs, supposing that each thread were bound to a CPU, + * there would exist a perfect hash function mapping threads to + * slots that eliminates collisions. When we reach capacity, we + * search for this mapping by randomly varying the hash codes of + * colliding threads. Because search is random, and collisions + * only become known via CAS failures, convergence can be slow, + * and because threads are typically not bound to CPUS forever, + * may not occur at all. However, despite these limitations, + * observed contention rates are typically low in these cases. + * + * It is possible for a Cell to become unused when threads that + * once hashed to it terminate, as well as in the case where + * doubling the table causes no thread to hash to it under + * expanded mask. We do not try to detect or remove such cells, + * under the assumption that for long-running instances, observed + * contention levels will recur, so the cells will eventually be + * needed again; and for short-lived ones, it does not matter. + */ + + private static final long serialVersionUID = -3403386352761423917L; + + /** + * Padded variant of AtomicLong supporting only raw accesses plus CAS. + * The value field is placed between pads, hoping that the JVM doesn't + * reorder them. + * + * JVM intrinsics note: It would be possible to use a release-only + * form of CAS here, if it were provided. + */ + static final class Cell { + volatile long p0, p1, p2, p3, p4, p5, p6; + volatile long value; + volatile long q0, q1, q2, q3, q4, q5, q6; + + Cell(long x) { value = x; } + + final boolean cas(long cmp, long val) { + return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); + } + + // Unsafe mechanics + private static final sun.misc.Unsafe UNSAFE; + private static final long valueOffset; + + static { + try { + UNSAFE = getUnsafe(); + Class ak = Cell.class; + valueOffset = UNSAFE.objectFieldOffset + (ak.getDeclaredField("value")); + } catch (Exception e) { + throw new Error(e); + } + } + + } + + /** + * Holder for the thread-local hash code. The code is initially + * random, but may be set to a different value upon collisions. + */ + static final class HashCode { + static final Random rng = new Random(); + int code; + + HashCode() { + int h = rng.nextInt(); // Avoid zero to allow xorShift rehash + code = (h == 0) ? 1 : h; + } + } + + /** + * The corresponding ThreadLocal class + */ + static final class ThreadHashCode extends ThreadLocal { + public HashCode initialValue() { return new HashCode(); } + } + + /** + * Static per-thread hash codes. Shared across all instances to + * reduce ThreadLocal pollution and because adjustments due to + * collisions in one table are likely to be appropriate for + * others. + */ + static final ThreadHashCode threadHashCode = new ThreadHashCode(); + + /** + * Number of CPUS, to place bound on table size + */ + static final int NCPU = Runtime.getRuntime().availableProcessors(); + + /** + * Table of cells. When non-null, size is a power of 2. + */ + transient volatile Cell[] cells; + + /** + * Base value, used mainly when there is no contention, but also as + * a fallback during table initialization races. Updated via CAS. + */ + transient volatile long base; + + /** + * Spinlock (locked via CAS) used when resizing and/or creating Cells. + */ + transient volatile int busy; + + /** + * Package-private default constructor + */ + Striped64() { + } + + /** + * CASes the base field. + */ + final boolean casBase(long cmp, long val) { + return UNSAFE.compareAndSwapLong(this, baseOffset, cmp, val); + } + + /** + * CASes the busy field from 0 to 1 to acquire lock. + */ + final boolean casBusy() { + return UNSAFE.compareAndSwapInt(this, busyOffset, 0, 1); + } + + /** + * Computes the function of current and new value. Subclasses + * should open-code this update function for most uses, but the + * virtualized form is needed within retryUpdate. + * + * @param currentValue the current value (of either base or a cell) + * @param newValue the argument from a user update call + * @return result of the update function + */ + abstract long fn(long currentValue, long newValue); + + /** + * Handles cases of updates involving initialization, resizing, + * creating new Cells, and/or contention. See above for + * explanation. This method suffers the usual non-modularity + * problems of optimistic retry code, relying on rechecked sets of + * reads. + * + * @param x the value + * @param hc the hash code holder + * @param wasUncontended false if CAS failed before call + */ + final void retryUpdate(long x, HashCode hc, boolean wasUncontended) { + int h = hc.code; + boolean collide = false; // True if last slot nonempty + for (; ; ) { + Cell[] as; + Cell a; + int n; + long v; + if ((as = cells) != null && (n = as.length) > 0) { + if ((a = as[(n - 1) & h]) == null) { + if (busy == 0) { // Try to attach new Cell + Cell r = new Cell(x); // Optimistically create + if (busy == 0 && casBusy()) { + boolean created = false; + try { // Recheck under lock + Cell[] rs; + int m, j; + if ((rs = cells) != null && + (m = rs.length) > 0 && + rs[j = (m - 1) & h] == null) { + rs[j] = r; + created = true; + } + } finally { + busy = 0; + } + if (created) { break; } + continue; // Slot is now non-empty + } + } + collide = false; + } else if (!wasUncontended) // CAS already known to fail + { + wasUncontended = true; // Continue after rehash + } else if (a.cas(v = a.value, fn(v, x))) { break; } else if (n >= NCPU || cells != as) { + collide = false; // At max size or stale + } else if (!collide) { collide = true; } else if (busy == 0 && casBusy()) { + try { + if (cells == as) { // Expand table unless stale + Cell[] rs = new Cell[n << 1]; + for (int i = 0; i < n; ++i) { rs[i] = as[i]; } + cells = rs; + } + } finally { + busy = 0; + } + collide = false; + continue; // Retry with expanded table + } + h ^= h << 13; // Rehash + h ^= h >>> 17; + h ^= h << 5; + } else if (busy == 0 && cells == as && casBusy()) { + boolean init = false; + try { // Initialize table + if (cells == as) { + Cell[] rs = new Cell[2]; + rs[h & 1] = new Cell(x); + cells = rs; + init = true; + } + } finally { + busy = 0; + } + if (init) { break; } + } else if (casBase(v = base, fn(v, x))) { + break; // Fall back on using base + } + } + hc.code = h; // Record index for next time + } + + /** + * Sets base and all cells to the given value. + */ + final void internalReset(long initialValue) { + Cell[] as = cells; + base = initialValue; + if (as != null) { + int n = as.length; + for (int i = 0; i < n; ++i) { + Cell a = as[i]; + if (a != null) { a.value = initialValue; } + } + } + } + + // Unsafe mechanics + private static final sun.misc.Unsafe UNSAFE; + private static final long baseOffset; + private static final long busyOffset; + + static { + try { + UNSAFE = getUnsafe(); + Class sk = Striped64.class; + baseOffset = UNSAFE.objectFieldOffset + (sk.getDeclaredField("base")); + busyOffset = UNSAFE.objectFieldOffset + (sk.getDeclaredField("busy")); + } catch (Exception e) { + throw new Error(e); + } + } + + /** + * Returns a sun.misc.Unsafe. Suitable for use in a 3rd party package. + * Replace with a simple call to Unsafe.getUnsafe when integrating + * into a jdk. + * + * @return a sun.misc.Unsafe + */ + private static sun.misc.Unsafe getUnsafe() { + try { + return sun.misc.Unsafe.getUnsafe(); + } catch (SecurityException se) { + try { + return java.security.AccessController.doPrivileged + (new java.security + .PrivilegedExceptionAction() { + public sun.misc.Unsafe run() throws Exception { + java.lang.reflect.Field f = sun.misc + .Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + return (sun.misc.Unsafe)f.get(null); + } + }); + } catch (java.security.PrivilegedActionException e) { + throw new RuntimeException("Could not initialize intrinsics", + e.getCause()); + } + } + } + +} \ No newline at end of file diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Window.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Window.java new file mode 100755 index 00000000..da31e8e6 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/Window.java @@ -0,0 +1,100 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic.base; + +/** + * Represents metrics data in a period of time window. + * + * @author jialiang.linjl + * @author Eric Zhao + */ +public class Window { + + private final LongAdder pass = new LongAdder(); + private final LongAdder block = new LongAdder(); + private final LongAdder exception = new LongAdder(); + private final LongAdder rt = new LongAdder(); + private final LongAdder success = new LongAdder(); + private final LongAdder minRt = new LongAdder(); + + public Window() { + minRt.add(4900); + } + + /** + * Clean the adders and reset window to provided start time. + * + * @param startTime the start time of the window + * @return new clean window + */ + Window resetTo(long startTime) { + pass.reset(); + block.reset(); + exception.reset(); + rt.reset(); + success.reset(); + minRt.reset(); + return this; + } + + public long pass() { + return pass.sum(); + } + + public long block() { + return block.sum(); + } + + public long exception() { + return exception.sum(); + } + + public long rt() { + return rt.sum(); + } + + public long minRt() { + return minRt.longValue(); + } + + public long success() { + return success.sum(); + } + + public void addPass() { + pass.add(1L); + } + + public void addException() { + exception.add(1L); + } + + public void addBlock() { + block.add(1L); + } + + public void addSuccess() { + success.add(1L); + } + + public void addRT(long rt) { + this.rt.add(rt); + + if (minRt.longValue() > rt) { + minRt.internalReset(rt); + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java new file mode 100755 index 00000000..7d7a0b51 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java @@ -0,0 +1,68 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic.base; + +/** + * Wrapper entity class for a period of time window. + * + * @param data type + * @author jialiang.linjl + * @author Eric Zhao + */ +public class WindowWrap { + + /** + * The length of the window. + */ + private final long windowLength; + + /** + * Start time of the window in milliseconds. + */ + private long windowStart; + + /** + * Statistic value. + */ + private T value; + + /** + * @param windowLength the time length of the window + * @param windowStart the start timestamp of the window + * @param value window data + */ + public WindowWrap(long windowLength, long windowStart, T value) { + this.windowLength = windowLength; + this.windowStart = windowStart; + this.value = value; + } + + public long windowLength() { + return windowLength; + } + + public long windowStart() { + return windowStart; + } + + public T value() { + return value; + } + + public void setValue(T value) { + this.value = value; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java new file mode 100755 index 00000000..6eb7df7e --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java @@ -0,0 +1,228 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic.metric; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.slots.statistic.base.Window; +import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; + +/** + * The basic metric class in Sentinel using a {@link WindowLeapArray} internal. + * + * @author jialiang.linjl + * @author Eric Zhao + */ +public class ArrayMetric implements Metric { + + private final WindowLeapArray data; + + public ArrayMetric(int windowLength, int interval) { + this.data = new WindowLeapArray(windowLength, interval); + } + + /** + * For unit test. + */ + public ArrayMetric(WindowLeapArray array) { + this.data = array; + } + + @Override + public long success() { + data.currentWindow(); + long success = 0; + + List list = data.values(); + for (Window window : list) { + success += window.success(); + } + return success; + } + + @Override + public long maxSuccess() { + data.currentWindow(); + long success = 0; + + List list = data.values(); + for (Window window : list) { + if (window.success() > success) { + success = window.success(); + } + } + return Math.max(success, 1); + } + + @Override + public long exception() { + data.currentWindow(); + long exception = 0; + List list = data.values(); + for (Window window : list) { + exception += window.exception(); + } + return exception; + } + + @Override + public long block() { + data.currentWindow(); + long block = 0; + List list = data.values(); + for (Window window : list) { + block += window.block(); + } + return block; + } + + @Override + public long pass() { + data.currentWindow(); + long pass = 0; + List list = data.values(); + + for (Window window : list) { + pass += window.pass(); + } + return pass; + } + + @Override + public long rt() { + data.currentWindow(); + long rt = 0; + List list = data.values(); + for (Window window : list) { + rt += window.rt(); + } + return rt; + } + + @Override + public long minRt() { + data.currentWindow(); + long rt = 4900; + List list = data.values(); + for (Window window : list) { + if (window.minRt() < rt) { + rt = window.minRt(); + } + } + + return Math.max(1, rt); + } + + @Override + public List details() { + List details = new ArrayList(); + data.currentWindow(); + for (WindowWrap window : data.list()) { + if (window == null) { + continue; + } + MetricNode node = new MetricNode(); + node.setBlockedQps(window.value().block()); + node.setException(window.value().exception()); + node.setPassedQps(window.value().pass()); + long passQps = window.value().success(); + node.setSuccessQps(passQps); + if (passQps != 0) { + node.setRt(window.value().rt() / passQps); + } else { + node.setRt(window.value().rt()); + } + node.setTimestamp(window.windowStart()); + details.add(node); + } + + return details; + } + + @Override + public Window[] windows() { + data.currentWindow(); + return data.values().toArray(new Window[data.values().size()]); + } + + @Override + public void addException() { + WindowWrap wrap = data.currentWindow(); + wrap.value().addException(); + } + + @Override + public void addBlock() { + WindowWrap wrap = data.currentWindow(); + wrap.value().addBlock(); + } + + @Override + public void addSuccess() { + WindowWrap wrap = data.currentWindow(); + wrap.value().addSuccess(); + } + + @Override + public void addPass() { + WindowWrap wrap = data.currentWindow(); + wrap.value().addPass(); + } + + @Override + public void addRT(long rt) { + WindowWrap wrap = data.currentWindow(); + wrap.value().addRT(rt); + } + + @Override + public void debugQps() { + data.currentWindow(); + StringBuilder sb = new StringBuilder(); + sb.append(Thread.currentThread().getId()).append("_"); + for (WindowWrap windowWrap : data.list()) { + + sb.append(windowWrap.windowStart()).append(":").append(windowWrap.value().pass()).append(":") + .append(windowWrap.value().block()); + sb.append(","); + + } + System.out.println(sb); + } + + @Override + public long previousWindowBlock() { + WindowWrap wrap = data.currentWindow(); + wrap = data.getPreviousWindow(); + if (wrap == null) { + return 0; + } + return wrap.value().block(); + } + + @Override + public long previousWindowPass() { + WindowWrap wrap = data.currentWindow(); + wrap = data.getPreviousWindow(); + if (wrap == null) { + return 0; + } + return wrap.value().pass(); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java new file mode 100755 index 00000000..fc56bf9e --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java @@ -0,0 +1,118 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic.metric; + +import java.util.List; + +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.slots.statistic.base.Window; + +/** + * Represents a basic structure recording invocation metrics of protected resources. + * + * @author jialiang.linjl + * @author Eric Zhao + */ +public interface Metric { + + /** + * Get total success count. + * + * @return success count + */ + long success(); + + long maxSuccess(); + + /** + * Get total exception count. + * + * @return exception count + */ + long exception(); + + /** + * Get total block count. + * + * @return block count + */ + long block(); + + /** + * Get total pass count. + * + * @return pass count + */ + long pass(); + + /** + * Get total RT. + * + * @return total RT + */ + long rt(); + + /** + * Get the minimal RT. + * + * @return minimal RT + */ + long minRt(); + + List details(); + + /** + * Get the raw window array. + * + * @return window metric array + */ + Window[] windows(); + + /** + * Increment by one the current exception count. + */ + void addException(); + + /** + * Increment by one the current blovk count. + */ + void addBlock(); + + /** + * Increment by one the current success count. + */ + void addSuccess(); + + /** + * Increment by one the current pass count. + */ + void addPass(); + + /** + * Add given RT to current total RT. + * + * @param rt RT + */ + void addRT(long rt); + + // Tool methods. + + void debugQps(); + + long previousWindowBlock(); + + long previousWindowPass(); +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/WindowLeapArray.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/WindowLeapArray.java new file mode 100755 index 00000000..70f60a92 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/WindowLeapArray.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.statistic.metric; + +import java.util.concurrent.locks.ReentrantLock; + +import com.alibaba.csp.sentinel.slots.statistic.base.LeapArray; +import com.alibaba.csp.sentinel.slots.statistic.base.Window; +import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; + +/** + * The fundamental data structure for metric statistics in a time window. + * + * @author jialiang.linjl + * @author Eric Zhao + */ +public class WindowLeapArray extends LeapArray { + + private final int timeLength; + + public WindowLeapArray(int windowLengthInMs, int intervalInSec) { + super(windowLengthInMs, intervalInSec); + timeLength = intervalInSec * 1000; + } + + private ReentrantLock addLock = new ReentrantLock(); + + @Override + public WindowWrap currentWindow(long time) { + + long timeId = time / windowLength; + // Calculate current index. + int idx = (int)(timeId % array.length()); + + // Cut the time to current window start. + time = time - time % windowLength; + + while (true) { + WindowWrap old = array.get(idx); + if (old == null) { + WindowWrap window = new WindowWrap(windowLength, time, new Window()); + if (array.compareAndSet(idx, null, window)) { + return window; + } else { + Thread.yield(); + } + } else if (time == old.windowStart()) { + return old; + } else if (time > old.windowStart()) { + if (addLock.tryLock()) { + try { + WindowWrap window = new WindowWrap(windowLength, time, new Window()); + if (array.compareAndSet(idx, old, window)) { + for (int i = 0; i < array.length(); i++) { + WindowWrap tmp = array.get(i); + if (tmp == null) { + continue; + } else { + if (tmp.windowStart() < time - timeLength) { + array.set(i, null); + } + } + } + return window; + } + } finally { + addLock.unlock(); + } + + } else { + Thread.yield(); + } + + } else if (time < old.windowStart()) { + return new WindowWrap(windowLength, time, new Window()); + } + } + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemBlockException.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemBlockException.java new file mode 100755 index 00000000..a6d6a835 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemBlockException.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.system; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * @author jialiang.linjl + */ +public class SystemBlockException extends BlockException { + + String resourceName; + + public String getResourceName() { + return resourceName; + } + + public SystemBlockException(String resourceName, String message, Throwable cause) { + super(message, cause); + this.resourceName = resourceName; + } + + public SystemBlockException(String resourceName, String ruleLimitApp) { + super(ruleLimitApp); + this.resourceName = resourceName; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRule.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRule.java new file mode 100755 index 00000000..318f6bef --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRule.java @@ -0,0 +1,170 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.system; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slots.block.AbstractRule; + +/** + *

+ * Sentinel System Rule makes the inbound traffic and capacity meet. It takes + * average RT, QPS and thread count of requests into account. And it also + * provides a measurement of system's load, but only available on Linux. + *

+ *

+ * We recommend to coordinate {@link #highestSystemLoad}, {@link #qps}, {@link #avgRt} + * and {@link #maxThread} to make sure your system run in safety level. + *

+ *

+ * To set the threshold appropriately, performance test may be needed. + *

+ * + * @author jialiang.linjl + * @see SystemRuleManager + */ +public class SystemRule extends AbstractRule { + + /** + * negative value means no threshold checking. + */ + private double highestSystemLoad = -1; + private double qps = -1; + private long avgRt = -1; + private long maxThread = -1; + + public double getQps() { + return qps; + } + + /** + * Set max total QPS. In a high concurrency condition, real passed QPS may be greater than max QPS set. + * The real passed QPS will nearly satisfy the following formula:
+ * + *
real passed QPS = QPS set + concurrent thread number
+ * + * @param qps max total QOS, values <= 0 are special for clearing the threshold. + */ + public void setQps(double qps) { + this.qps = qps; + } + + public long getMaxThread() { + return maxThread; + } + + /** + * Set max PARALLEL working thread. When concurrent thread number is greater than {@code maxThread} only + * maxThread will run in parallel. + * + * @param maxThread max parallel thread number, values <= 0 are special for clearing the threshold. + */ + public void setMaxThread(long maxThread) { + this.maxThread = maxThread; + } + + public long getAvgRt() { + return avgRt; + } + + /** + * Set max average RT(response time) of all passed requests. + * + * @param avgRt max average response time, values <= 0 are special for clearing the threshold. + */ + public void setAvgRt(long avgRt) { + this.avgRt = avgRt; + } + + public double getHighestSystemLoad() { + return highestSystemLoad; + } + + /** + *

+ * Set highest load. The load is not same as Linux system load, which is not sensitive enough. + * To calculate the load, both Linux system load, current global response time and global QPS will be considered, + * which means that we need to coordinate with {@link #setAvgRt(long)} and {@link #setQps(double)} + *

+ *

+ * Note that this parameter is only available on Unix like system. + *

+ * + * @param highestSystemLoad highest system load, values <= 0 are special for clearing the threshold. + * @see SystemRuleManager + */ + public void setHighestSystemLoad(double highestSystemLoad) { + this.highestSystemLoad = highestSystemLoad; + } + + @Override + public boolean passCheck(Context context, DefaultNode node, int count, Object... args) { + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SystemRule)) { + return false; + } + if (!super.equals(o)) { + return false; + } + + SystemRule that = (SystemRule)o; + + if (Double.compare(that.highestSystemLoad, highestSystemLoad) != 0) { + return false; + } + + if (Double.compare(that.qps, qps) != 0) { + return false; + } + + if (avgRt != that.avgRt) { + return false; + } + return maxThread == that.maxThread; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(highestSystemLoad); + result = 31 * result + (int)(temp ^ (temp >>> 32)); + + temp = Double.doubleToLongBits(qps); + result = 31 * result + (int)(temp ^ (temp >>> 32)); + + result = 31 * result + (int)(avgRt ^ (avgRt >>> 32)); + result = 31 * result + (int)(maxThread ^ (maxThread >>> 32)); + return result; + } + + @Override + public String toString() { + return "SystemRule{" + + "highestSystemLoad=" + highestSystemLoad + + ", qps=" + qps + + ", avgRt=" + avgRt + + ", maxThread=" + maxThread + + "}"; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java new file mode 100755 index 00000000..c8e6378b --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRuleManager.java @@ -0,0 +1,303 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.system; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.property.SimplePropertyListener; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + *

+ * Sentinel System Rule makes the inbound traffic and capacity meet. It takes + * average rt, qps, thread count of incoming requests into account. And it also + * provides a measurement of system's load, but only available on Linux. + *

+ *

+ * rt, qps, thread count is easy to understand. If the incoming requests' + * rt,qps, thread count exceeds its threshold, the requests will be + * rejected.however, we use a different method to calculate the load. + *

+ *

+ * Consider the system as a pipeline,transitions between constraints result in + * three different regions (traffic-limited, capacity-limited and danger area) + * with qualitatively different behavior. When there isn’t enough request in + * flight to fill the pipe, RTprop determines behavior; otherwise, the system + * capacity dominates. Constraint lines intersect at inflight = Capacity × + * RTprop. Since the pipe is full past this point, the inflight –capacity excess + * creates a queue, which results in the linear dependence of RTT on inflight + * traffic and an increase in system load.In danger area, system will stop + * responding.
+ * Referring to BBR algorithm to learn more. + *

+ *

+ * Note that {@link SystemRule} only effect on inbound requests, outbound traffic + * will not limit by {@link SystemRule} + *

+ * + * @author jialiang.linjl + * @author leyou + */ +public class SystemRuleManager { + + private static volatile double highestSystemLoad = Double.MAX_VALUE; + private static volatile double qps = Double.MAX_VALUE; + private static volatile long maxRt = Long.MAX_VALUE; + private static volatile long maxThread = Long.MAX_VALUE; + /** + * mark whether the threshold are set by user. + */ + private static volatile boolean highestSystemLoadIsSet = false; + private static volatile boolean qpsIsSet = false; + private static volatile boolean maxRtIsSet = false; + private static volatile boolean maxThreadIsSet = false; + + static AtomicBoolean checkSystemStatus = new AtomicBoolean(false); + + private static SystemStatusListener statusListener = null; + private final static SystemPropertyListener listener = new SystemPropertyListener(); + private static SentinelProperty> currentProperty = new DynamicSentinelProperty>(); + + private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + static { + checkSystemStatus.set(false); + statusListener = new SystemStatusListener(); + scheduler.scheduleAtFixedRate(statusListener, 5, 1, TimeUnit.SECONDS); + currentProperty.addListener(listener); + } + + /** + * Listen to the {@link SentinelProperty} for {@link SystemRule}s. The property is the source + * of {@link SystemRule}s. System rules can also be set by {@link #loadRules(List)} directly. + * + * @param property the property to listen. + */ + public static void register2Property(SentinelProperty> property) { + synchronized (listener) { + currentProperty.removeListener(listener); + property.addListener(listener); + currentProperty = property; + } + } + + /** + * Load {@link SystemRule}s, former rules will be replaced. + * + * @param rules new rules to load. + */ + public static void loadRules(List rules) { + currentProperty.updateValue(rules); + } + + /** + * Get a copy of the rules. + * + * @return a new copy of the rules. + */ + public static List getRules() { + + List result = new ArrayList(); + if (!checkSystemStatus.get()) { + return result; + } + + if (highestSystemLoadIsSet) { + SystemRule loadRule = new SystemRule(); + loadRule.setHighestSystemLoad(highestSystemLoad); + result.add(loadRule); + } + + if (maxRtIsSet) { + SystemRule rtRule = new SystemRule(); + rtRule.setAvgRt(maxRt); + result.add(rtRule); + } + + if (maxThreadIsSet) { + SystemRule threadRule = new SystemRule(); + threadRule.setMaxThread(maxThread); + result.add(threadRule); + } + + if (qpsIsSet) { + SystemRule qpsRule = new SystemRule(); + qpsRule.setQps(qps); + result.add(qpsRule); + } + + return result; + } + + public static double getQps() { + return qps; + } + + public static void setQps(double qps) { + SystemRuleManager.qps = qps; + } + + public static long getMaxRt() { + return maxRt; + } + + public static long getMaxThread() { + return maxThread; + } + + static class SystemPropertyListener extends SimplePropertyListener> { + + @Override + public void configUpdate(List rules) { + restoreSetting(); + // systemRules = rules; + if (rules != null && rules.size() >= 1) { + for (SystemRule rule : rules) { + loadSystemConf(rule); + } + } else { + checkSystemStatus.set(false); + } + + RecordLog.info("current system system status : " + checkSystemStatus.get()); + RecordLog.info("current highestSystemLoad status : " + highestSystemLoad); + RecordLog.info("current maxRt : " + maxRt); + RecordLog.info("current maxThread : " + maxThread); + RecordLog.info("current qps : " + qps); + } + + protected void restoreSetting() { + checkSystemStatus.set(false); + + // should restore changes + highestSystemLoad = Double.MAX_VALUE; + maxRt = Long.MAX_VALUE; + maxThread = Long.MAX_VALUE; + qps = Double.MAX_VALUE; + + highestSystemLoadIsSet = false; + maxRtIsSet = false; + maxThreadIsSet = false; + qpsIsSet = false; + } + + } + + public static Boolean getCheckSystemStatus() { + return checkSystemStatus.get(); + } + + public static double getHighestSystemLoad() { + return highestSystemLoad; + } + + public static void setHighestSystemLoad(double highestSystemLoad) { + SystemRuleManager.highestSystemLoad = highestSystemLoad; + } + + public static void loadSystemConf(SystemRule rule) { + + boolean checkStatus = false; + // 首先判断是否有效 + + if (rule.getHighestSystemLoad() > 0) { + highestSystemLoad = Math.min(highestSystemLoad, rule.getHighestSystemLoad()); + highestSystemLoadIsSet = true; + checkStatus = true; + } + + if (rule.getAvgRt() > 0) { + maxRt = Math.min(maxRt, rule.getAvgRt()); + maxRtIsSet = true; + checkStatus = true; + } + if (rule.getMaxThread() > 0) { + maxThread = Math.min(maxThread, rule.getMaxThread()); + maxThreadIsSet = true; + checkStatus = true; + } + + if (rule.getQps() > 0) { + qps = Math.min(qps, rule.getQps()); + qpsIsSet = true; + checkStatus = true; + } + + checkSystemStatus.set(checkStatus); + + } + + /** + * Apply {@link SystemRule} to the resource. Only inbound traffic will be checked. + * + * @param resourceWrapper the resource. + * @throws BlockException when any system rule's threshold is exceeded. + */ + public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException { + + // 确定开关开了 + if (checkSystemStatus.get() == false) { + return; + } + + // for inbound traffic only + if (resourceWrapper.getType() != EntryType.IN) { + return; + } + + // total qps + double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps(); + if (currentQps > qps) { + throw new SystemBlockException(resourceWrapper.getName(), "qps"); + } + + // total thread + int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum(); + if (currentThread > maxThread) { + throw new SystemBlockException(resourceWrapper.getName(), "thread"); + } + + double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt(); + if (rt > maxRt) { + throw new SystemBlockException(resourceWrapper.getName(), "rt"); + } + + // 完全按照RT,BBR算法来 + if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) { + if (currentThread > 1 && + currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) { + throw new SystemBlockException(resourceWrapper.getName(), "load"); + } + } + + } + + public static double getCurrentSystemAvgLoad() { + return statusListener.getSystemAverageLoad(); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemSlot.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemSlot.java new file mode 100755 index 00000000..e7519270 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemSlot.java @@ -0,0 +1,44 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.system; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * A {@link ProcessorSlot} that dedicates to {@link SystemRule} checking. + * + * @author jialiang.linjl + * @author leyou + */ +public class SystemSlot extends AbstractLinkedProcessorSlot { + + @Override + public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) + throws Throwable { + SystemRuleManager.checkSystem(resourceWrapper); + fireEntry(context, resourceWrapper, node, count, args); + } + + @Override + public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) { + fireExit(context, resourceWrapper, count, args); + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java new file mode 100755 index 00000000..2ccf6c63 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemStatusListener.java @@ -0,0 +1,68 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.system; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; + +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.Constants; + +/** + * @author jialiang.linjl + */ +public class SystemStatusListener implements Runnable { + + volatile double currentLoad = -1; + + volatile String reason = StringUtil.EMPTY; + + static final int processor = ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors(); + + public double getSystemAverageLoad() { + return currentLoad; + } + + @Override + public void run() { + try { + if (!SystemRuleManager.getCheckSystemStatus()) { + return; + } + + // system average load + OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean(); + currentLoad = operatingSystemMXBean.getSystemLoadAverage(); + + StringBuilder sb = new StringBuilder(); + if (currentLoad > SystemRuleManager.getHighestSystemLoad()) { + sb.append("load:").append(currentLoad).append(";"); + sb.append("qps:").append(Constants.ENTRY_NODE.passQps()).append(";"); + sb.append("rt:").append(Constants.ENTRY_NODE.avgRt()).append(";"); + sb.append("thread:").append(Constants.ENTRY_NODE.curThreadNum()).append(";"); + sb.append("success:").append(Constants.ENTRY_NODE.successQps()).append(";"); + sb.append("minRt:").append(Constants.ENTRY_NODE.minRt()).append(";"); + sb.append("maxSuccess:").append(Constants.ENTRY_NODE.maxSuccessQps()).append(";"); + RecordLog.info(sb.toString()); + } + + } catch (Throwable e) { + RecordLog.info("could not get system error ", e); + } + } + +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java new file mode 100755 index 00000000..d616f29f --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/AppNameUtil.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +import java.io.File; + +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + * Util class for getting application name. This class uses the flowing order to get app's name: + * + *
    + *
  1. get {@code project.name} from System Properties, if not null, use the value as app name;
  2. + *
  3. get {@code sun.java.command} from System properties, remove path, arguments and ".jar" or ".JAR" + * suffix, use the result as app name. Note that whitespace in file name or path is not allowed, or a + * wrong app name may be gotten, For example: + *

    + * + * "test.Main" -> test.Main
    + * "/target/test.Main" -> test.Main
    + * "/target/test.Main args1" -> test.Main
    + * "Main.jar" -> Main
    + * "/target/Main.JAR args1" -> Main
    + * "Mai n.jar" -> Mai // whitespace in file name is not allowed
    + *
    + *

    + *
  4. + *
+ * + * @author Eric Zhao + * @author leyou + */ +public final class AppNameUtil { + + public static final String APP_NAME = "project.name"; + public static final String SUN_JAVA_COMMAND = "sun.java.command"; + + private static String appName; + + private AppNameUtil() { + } + + static { + resolveAppName(); + RecordLog.info("app name resolved: " + appName); + } + + public static void resolveAppName() { + String app = System.getProperty(APP_NAME); + // use -Dproject.name first + if (!isEmpty(app)) { + appName = app; + return; + } + + // parse sun.java.command property + String command = System.getProperty(SUN_JAVA_COMMAND); + if (isEmpty(command)) { + return; + } + command = command.split("\\s")[0]; + if (command.contains(File.separator)) { + String[] strs = command.split(File.separator); + command = strs[strs.length - 1]; + } + if (command.endsWith(".jar") || command.endsWith(".JAR")) { + command = command.substring(0, command.length() - 4); + } + appName = command; + } + + public static String getAppName() { + return appName; + } + + private static boolean isEmpty(String str) { + return str == null || "".equals(str); + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/HostNameUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/HostNameUtil.java new file mode 100755 index 00000000..fb0c3581 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/HostNameUtil.java @@ -0,0 +1,80 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + * Get host name and ip of the host. + * + * @author leyou + */ +public final class HostNameUtil { + + private static String ip; + private static String hostName; + + static { + try { + // Init the host information. + resolveHost(); + } catch (Exception e) { + RecordLog.info("Failed to get local host", e); + } + } + + private static void resolveHost() throws Exception { + InetAddress addr = InetAddress.getLocalHost(); + hostName = addr.getHostName(); + ip = addr.getHostAddress(); + if (addr.isLoopbackAddress()) { + // find the first IPv4 Address that not loopback + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface in = interfaces.nextElement(); + Enumeration addrs = in.getInetAddresses(); + while (addrs.hasMoreElements()) { + InetAddress address = addrs.nextElement(); + if (!address.isLoopbackAddress() && address instanceof Inet4Address) { + ip = address.getHostAddress(); + } + } + } + } + } + + public static String getIp() { + return ip; + } + + public static String getHostName() { + return hostName; + } + + public static String getConfigString() { + return "{\n" + + "\t\"machine\": \"" + hostName + "\",\n" + + "\t\"ip\": \"" + ip + "\"\n" + + "}"; + } + + private HostNameUtil() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/IdUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/IdUtil.java new file mode 100755 index 00000000..0dfaded4 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/IdUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +/** + * @author qinan.qn + */ +public final class IdUtil { + + public static String truncate(String id) { + IdLexer lexer = new IdLexer(id); + StringBuilder sb = new StringBuilder(); + String r; + String temp = ""; + while ((r = lexer.nextToken()) != null) { + if ("(".equals(r) || ")".equals(r) || ",".equals(r)) { + sb.append(temp).append(r); + temp = ""; + } else if (!".".equals(r)) { + temp = r; + } + } + + return sb.toString(); + } + + private static class IdLexer { + private String id; + private int idx = 0; + + IdLexer(String id) { + this.id = id; + } + + String nextToken() { + int oldIdx = idx; + String result = null; + while (idx != id.length()) { + char curChar = id.charAt(idx); + if (curChar == '.' || curChar == '(' || curChar == ')' || curChar == ',') { + if (idx == oldIdx) { + result = String.valueOf(curChar); + ++idx; + break; + } else { + result = id.substring(oldIdx, idx); + break; + } + } + ++idx; + } + return result; + } + } + + private IdUtil() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/MethodUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/MethodUtil.java new file mode 100755 index 00000000..e4195da1 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/MethodUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/*** + * Util class for processing {@link Method}. + * + * @author youji.zj + */ +public final class MethodUtil { + + private static Map methodNameMap = new HashMap(); + + private static final Object LOCK = new Object(); + + /** + * Parse and get the method name. + */ + public static String getMethodName(Method method) { + String methodName = methodNameMap.get(method); + if (methodName == null) { + synchronized (LOCK) { + methodName = methodNameMap.get(method); + if (methodName == null) { + StringBuilder sb = new StringBuilder(); + + String className = method.getDeclaringClass().getName(); + String name = method.getName(); + Class[] params = method.getParameterTypes(); + sb.append(className).append(":").append(name); + sb.append("("); + + int paramPos = 0; + for (Class clazz : params) { + sb.append(clazz.getName()); + if (++paramPos < params.length) { + sb.append(","); + } + } + sb.append(")"); + methodName = sb.toString(); + + HashMap newMap = new HashMap(methodNameMap); + newMap.put(method, methodName); + methodNameMap = newMap; + } + } + } + return methodName; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/PidUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/PidUtil.java new file mode 100755 index 00000000..31efefb7 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/PidUtil.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +import java.lang.management.ManagementFactory; + +/** + * Util class providing pid of current process. + */ +public final class PidUtil { + + public static int getPid() { + String name = ManagementFactory.getRuntimeMXBean().getName(); + return Integer.parseInt(name.split("@")[0]); + } + + private PidUtil() {} +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/StringUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/StringUtil.java new file mode 100755 index 00000000..a355ef56 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/StringUtil.java @@ -0,0 +1,120 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +/*** + * Util class providing operations on {@link String}. + * + * @author youji.zj + */ +public final class StringUtil { + + public static final String EMPTY = ""; + + public static boolean equalsIgnoreCase(final CharSequence str1, final CharSequence str2) { + if (str1 == null || str2 == null) { + return str1 == str2; + } else if (str1 == str2) { + return true; + } else if (str1.length() != str2.length()) { + return false; + } else { + return regionMatches(str1, true, 0, str2, 0, str1.length()); + } + } + + public static boolean equals(String str1, String str2) { + return str1 == null ? str2 == null : str1.equals(str2); + } + + public static boolean isBlank(String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if ((!Character.isWhitespace(str.charAt(i)))) { + return false; + } + } + return true; + } + + public static boolean isNotBlank(String str) { + return !isBlank(str); + } + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + public static String trimToEmpty(String str) { + return str == null ? EMPTY : str.trim(); + } + + public static String trim(String str) { + return str == null ? null : str.trim(); + } + + private static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart, + final CharSequence substring, final int start, final int length) { + if (cs instanceof String && substring instanceof String) { + return ((String)cs).regionMatches(ignoreCase, thisStart, (String)substring, start, length); + } + int index1 = thisStart; + int index2 = start; + int tmpLen = length; + + // Extract these first so we detect NPEs the same as the java.lang.String version + final int srcLen = cs.length() - thisStart; + final int otherLen = substring.length() - start; + + // Check for invalid parameters + if (thisStart < 0 || start < 0 || length < 0) { + return false; + } + + // Check that the regions are long enough + if (srcLen < length || otherLen < length) { + return false; + } + + while (tmpLen-- > 0) { + final char c1 = cs.charAt(index1++); + final char c2 = substring.charAt(index2++); + + if (c1 == c2) { + continue; + } + + if (!ignoreCase) { + return false; + } + + // The same check as in String.regionMatches(): + if (Character.toUpperCase(c1) != Character.toUpperCase(c2) + && Character.toLowerCase(c1) != Character.toLowerCase(c2)) { + return false; + } + } + + return true; + } +} diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/TimeUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/TimeUtil.java new file mode 100755 index 00000000..a51276a0 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/TimeUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.util; + +import java.util.concurrent.TimeUnit; + +/** + * Provides millisecond-level time of OS. + * + * @author qinan.qn + */ +public final class TimeUtil { + + private static volatile long currentTimeMillis; + + static { + currentTimeMillis = System.currentTimeMillis(); + Thread daemon = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + currentTimeMillis = System.currentTimeMillis(); + try { + TimeUnit.MILLISECONDS.sleep(1); + } catch (Throwable e) { + + } + } + } + }); + daemon.setDaemon(true); + daemon.setName("sentinel-time-tick-thread"); + daemon.start(); + } + + public static long currentTimeMillis() { + return currentTimeMillis; + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConfigPropertyHelper.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConfigPropertyHelper.java new file mode 100755 index 00000000..773722e9 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ConfigPropertyHelper.java @@ -0,0 +1,69 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.Properties; + +import com.alibaba.csp.sentinel.log.LogBase; +import com.alibaba.csp.sentinel.util.AppNameUtil; + +/** + * Helper class for executing a task within a config context via properties file. + * + * @author Eric Zhao + */ +public final class ConfigPropertyHelper { + + public static void setAppNameProperty(String appName) { + System.setProperty(AppNameUtil.APP_NAME, appName); + } + + public static void clearAppNameProperty() { + System.clearProperty(AppNameUtil.APP_NAME); + } + + public static void runWithConfig(Properties prop, String appName, Task task) throws Exception { + if (prop == null || appName == null || "".equals(appName)) { + throw new IllegalArgumentException("Prop and appName cannot be empty"); + } + // Set application name property. + setAppNameProperty(appName); + // Save the config. + String path = LogBase.getLogBaseDir() + appName + ".properties"; + File file = new File(path); + if (!file.exists()) { + file.createNewFile(); + } + OutputStream outputStream = new FileOutputStream(file); + prop.store(outputStream,""); + outputStream.close(); + // Run the procedure. + task.run(); + // Clean-up. + file.delete(); + // Clear application name property. + clearAppNameProperty(); + } + + public interface Task { + void run() throws Exception; + } + + private ConfigPropertyHelper() {} +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java new file mode 100755 index 00000000..cb09f44a --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.context.ContextUtil; + +/** + * @author jialiang.linjl + */ +public class ContextTest { + + @Test + public void testEnterContext() { + // TODO: rewrite this unit test + ContextUtil.enter("entry", "origin"); + + ContextUtil.exit(); + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java new file mode 100755 index 00000000..e6c9935f --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/RecordLogTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import com.alibaba.csp.sentinel.log.RecordLog; + +import org.junit.Test; + +/** + * @author xuyue + */ +public class RecordLogTest { + + @Test + public void testLogException() { + Exception e = new Exception("ex"); + RecordLog.info("Error", e); + } + + @Test + public void testLogRolling() { + int count = 1000; + while (--count > 0) { + RecordLog.info("Count " + count); + } + } + +} \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java new file mode 100755 index 00000000..7f8787a5 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphOTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; + +/** + * Test cases for {@link SphO}. + * + * @author jialiang.linjl + */ +public class SphOTest { + + @Test + public void testStringEntryNormal() { + if (SphO.entry("resourceName")) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); + } finally { + SphO.exit(); + } + } + } + + @Test + public void testMethodEntryNormal() throws NoSuchMethodException, SecurityException { + Method method = SphOTest.class.getMethod("testMethodEntryNormal"); + if (SphO.entry(method)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), + "com.alibaba.csp.sentinel.SphOTest:testMethodEntryNormal()")); + } finally { + SphO.exit(); + } + } + } + + @Test + public void testStringEntryCount() { + if (SphO.entry("resourceName", 2)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.OUT); + } finally { + SphO.exit(2); + } + } + } + + @Test + public void testMethodEntryCount() throws NoSuchMethodException, SecurityException { + Method method = SphOTest.class.getMethod("testMethodEntryCount"); + if (SphO.entry(method, 2)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), + "com.alibaba.csp.sentinel.SphOTest:testMethodEntryCount()")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.OUT); + } finally { + SphO.exit(2); + } + } + } + + @Test + public void testStringEntryType() { + if (SphO.entry("resourceName", EntryType.IN)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + } finally { + SphO.exit(); + } + } + } + + @Test + public void testMethodEntryType() throws NoSuchMethodException, SecurityException { + Method method = SphOTest.class.getMethod("testMethodEntryType"); + if (SphO.entry(method, EntryType.IN)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), + "com.alibaba.csp.sentinel.SphOTest:testMethodEntryType()")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + } finally { + SphO.exit(); + } + } + } + + @Test + public void testStringEntryTypeCount() { + if (SphO.entry("resourceName", EntryType.IN, 2)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + } finally { + SphO.exit(2); + } + } + } + + @Test + public void testMethodEntryTypeCount() throws NoSuchMethodException, SecurityException { + Method method = SphOTest.class.getMethod("testMethodEntryTypeCount"); + if (SphO.entry(method, EntryType.IN, 2)) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), + "com.alibaba.csp.sentinel.SphOTest:testMethodEntryTypeCount()")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + } finally { + SphO.exit(2); + } + } + } + + @Test + public void testStringEntryAll() { + if (SphO.entry("resourceName", EntryType.IN, 2, "hello1", "hello2")) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), "resourceName")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + } finally { + SphO.exit(2, "hello1", "hello2"); + } + } + } + + @Test + public void testMethodEntryAll() throws NoSuchMethodException, SecurityException { + Method method = SphOTest.class.getMethod("testMethodEntryAll"); + if (SphO.entry(method, EntryType.IN, 2, "hello1", "hello2")) { + try { + assertTrue(StringUtil.equalsIgnoreCase( + ContextUtil.getContext().getCurEntry().getResourceWrapper().getName(), + "com.alibaba.csp.sentinel.SphOTest:testMethodEntryAll()")); + assertSame(ContextUtil.getContext().getCurEntry().getResourceWrapper().getType(), EntryType.IN); + } finally { + SphO.exit(2, "hello1", "hello2"); + } + } + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java new file mode 100755 index 00000000..a1a58a1a --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/SphUTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/** + * Test cases for {@link SphU}. + * + * @author jialiang.linjl + */ +public class SphUTest { + + @Test + public void testStringEntryNormal() throws BlockException { + Entry e = SphU.entry("resourceName"); + + assertNotNull(e); + assertEquals(e.resourceWrapper.getName(), "resourceName"); + assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(ContextUtil.getContext().getName(), Constants.CONTEXT_DEFAULT_NAME); + + e.exit(); + } + + @Test + public void testMethodEntryNormal() throws BlockException, NoSuchMethodException, SecurityException { + Method method = SphUTest.class.getMethod("testMethodEntryNormal"); + Entry e = SphU.entry(method); + + assertNotNull(e); + assertTrue(StringUtil + .equalsIgnoreCase(e.resourceWrapper.getName(), + "com.alibaba.csp.sentinel.SphUTest:testMethodEntryNormal()")); + assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(ContextUtil.getContext().getName(), Constants.CONTEXT_DEFAULT_NAME); + + e.exit(); + } + + @Test(expected = ErrorEntryFreeException.class) + public void testStringEntryNotPairedException() throws BlockException { + Entry e = SphU.entry("resourceName"); + Entry e1 = SphU.entry("resourceName"); + + if (e != null) { + e.exit(); + } + if (e1 != null) { + e1.exit(); + } + } + + @Test + public void testStringEntryCount() throws BlockException { + Entry e = SphU.entry("resourceName", 2); + + assertNotNull(e); + assertEquals("resourceName", e.resourceWrapper.getName()); + assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + assertEquals(ContextUtil.getContext().getName(), Constants.CONTEXT_DEFAULT_NAME); + + e.exit(2); + } + + @Test + public void testMethodEntryCount() throws BlockException, NoSuchMethodException, SecurityException { + Method method = SphUTest.class.getMethod("testMethodEntryNormal"); + Entry e = SphU.entry(method, 2); + + assertNotNull(e); + assertTrue(StringUtil + .equalsIgnoreCase(e.resourceWrapper.getName(), + "com.alibaba.csp.sentinel.SphUTest:testMethodEntryNormal()")); + assertEquals(e.resourceWrapper.getType(), EntryType.OUT); + + e.exit(2); + } + + @Test + public void testStringEntryType() throws BlockException { + Entry e = SphU.entry("resourceName", EntryType.IN); + + assertSame(e.resourceWrapper.getType(), EntryType.IN); + + e.exit(); + } + + @Test + public void testMethodEntryType() throws BlockException, NoSuchMethodException, SecurityException { + Method method = SphUTest.class.getMethod("testMethodEntryNormal"); + Entry e = SphU.entry(method, EntryType.IN); + + assertSame(e.resourceWrapper.getType(), EntryType.IN); + + e.exit(); + } + + @Test + public void testStringEntryCountType() throws BlockException { + Entry e = SphU.entry("resourceName", EntryType.IN, 2); + + assertSame(e.resourceWrapper.getType(), EntryType.IN); + + e.exit(2); + } + + @Test + public void testMethodEntryCountType() throws BlockException, NoSuchMethodException, SecurityException { + Method method = SphUTest.class.getMethod("testMethodEntryNormal"); + Entry e = SphU.entry(method, EntryType.IN, 2); + + assertSame(e.resourceWrapper.getType(), EntryType.IN); + + e.exit(); + } + + @Test + public void testStringEntryAll() throws BlockException { + final String arg0 = "foo"; + final String arg1 = "baz"; + Entry e = SphU.entry("resourceName", EntryType.IN, 2, arg0, arg1); + assertSame(e.resourceWrapper.getType(), EntryType.IN); + + e.exit(2, arg0, arg1); + } + + @Test + public void testMethodEntryAll() throws BlockException, NoSuchMethodException, SecurityException { + final String arg0 = "foo"; + final String arg1 = "baz"; + Method method = SphUTest.class.getMethod("testMethodEntryNormal"); + Entry e = SphU.entry(method, EntryType.IN, 2, arg0, arg1); + + assertSame(e.resourceWrapper.getType(), EntryType.IN); + + e.exit(2, arg0, arg1); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/ArrayMetricTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/ArrayMetricTest.java new file mode 100755 index 00000000..8bfbff4c --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/ArrayMetricTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.base.metric; + +import java.util.ArrayList; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.slots.statistic.base.Window; +import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; +import com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric; +import com.alibaba.csp.sentinel.slots.statistic.metric.WindowLeapArray; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +/** + * Test cases for {@link ArrayMetric}. + * + * @author Eric Zhao + */ +public class ArrayMetricTest { + + private final int windowLengthInMs = 500; + private final int intervalInSec = 1; + + @Test + public void testOperateArrayMetric() { + WindowLeapArray leapArray = mock(WindowLeapArray.class); + final WindowWrap windowWrap = new WindowWrap(windowLengthInMs, 0, new Window()); + when(leapArray.currentWindow()).thenReturn(windowWrap); + when(leapArray.values()).thenReturn(new ArrayList() {{ add(windowWrap.value()); }}); + + ArrayMetric metric = new ArrayMetric(leapArray); + + final int expectedPass = 9; + final int expectedBlock = 2; + final int expectedSuccess = 9; + final int expectedException = 6; + final int expectedRt = 21; + + metric.addRT(expectedRt); + for (int i = 0; i < expectedPass; i++) { + metric.addPass(); + } + for (int i = 0; i < expectedBlock; i++) { + metric.addBlock(); + } + for (int i = 0; i < expectedSuccess; i++) { + metric.addSuccess(); + } + for (int i = 0; i < expectedException; i++) { + metric.addException(); + } + + assertEquals(expectedPass, metric.pass()); + assertEquals(expectedBlock, metric.block()); + assertEquals(expectedSuccess, metric.success()); + assertEquals(expectedException, metric.exception()); + assertEquals(expectedRt, metric.rt()); + } +} \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/WindowLeapArrayTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/WindowLeapArrayTest.java new file mode 100755 index 00000000..84390c57 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/base/metric/WindowLeapArrayTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.base.metric; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.slots.statistic.base.Window; +import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; +import com.alibaba.csp.sentinel.slots.statistic.metric.WindowLeapArray; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link WindowLeapArray}. + * + * @author Eric Zhao + */ +public class WindowLeapArrayTest { + + private final int windowLengthInMs = 1000; + private final int intervalInSec = 2; + + @Test + public void testNewWindow() { + WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + long time = TimeUtil.currentTimeMillis(); + WindowWrap window = leapArray.currentWindow(time); + + assertEquals(window.windowLength(), windowLengthInMs); + assertEquals(window.windowStart(), time - time % windowLengthInMs); + assertNotNull(window.value()); + assertEquals(0L, window.value().pass()); + } + + @Test + public void testLeapArrayWindowStart() { + WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + long firstTime = TimeUtil.currentTimeMillis(); + long previousWindowStart = firstTime - firstTime % windowLengthInMs; + + WindowWrap window = leapArray.currentWindow(firstTime); + + assertEquals(windowLengthInMs, window.windowLength()); + assertEquals(previousWindowStart, window.windowStart()); + } + + @Test + public void testWindowAfterOneInterval() { + WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + long firstTime = TimeUtil.currentTimeMillis(); + long previousWindowStart = firstTime - firstTime % windowLengthInMs; + WindowWrap window = leapArray.currentWindow(previousWindowStart); + + assertEquals(windowLengthInMs, window.windowLength()); + assertEquals(previousWindowStart, window.windowStart()); + + Window currentWindow = window.value(); + assertNotNull(currentWindow); + + currentWindow.addPass(); + currentWindow.addBlock(); + assertEquals(1L, currentWindow.pass()); + assertEquals(1L, currentWindow.block()); + + long middleTime = previousWindowStart + windowLengthInMs / 2; + + window = leapArray.currentWindow(middleTime); + assertEquals(previousWindowStart, window.windowStart()); + + Window middleWindow = window.value(); + middleWindow.addPass(); + assertSame(currentWindow, middleWindow); + assertEquals(2L, middleWindow.pass()); + assertEquals(1L, middleWindow.block()); + + long nextTime = middleTime + windowLengthInMs / 2; + window = leapArray.currentWindow(nextTime); + assertEquals(windowLengthInMs, window.windowLength()); + assertEquals(windowLengthInMs, window.windowStart() - previousWindowStart); + + currentWindow = window.value(); + assertNotNull(currentWindow); + assertEquals(0L, currentWindow.pass()); + assertEquals(0L, currentWindow.block()); + } + + @Test + public void testWindowDeprecatedRefresh() { + WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + final int len = intervalInSec * 1000 / windowLengthInMs; + long firstTime = TimeUtil.currentTimeMillis(); + List> firstIterWindowList = new ArrayList>(len); + for (int i = 0; i < len; i++) { + WindowWrap w = leapArray.currentWindow(firstTime + windowLengthInMs * i); + w.value().addPass(); + firstIterWindowList.add(i, w); + } + + for (int i = len; i < len * 2; i++) { + WindowWrap w = leapArray.currentWindow(firstTime + windowLengthInMs * i); + assertNotSame(w, firstIterWindowList.get(i - len)); + } + } + + @Test + public void testMultiThreadUpdateEmptyWindow() throws Exception { + final long time = TimeUtil.currentTimeMillis(); + final int nThreads = 16; + final WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + final CountDownLatch latch = new CountDownLatch(nThreads); + Runnable task = new Runnable() { + @Override + public void run() { + leapArray.currentWindow(time).value().addPass(); + latch.countDown(); + } + }; + + for (int i = 0; i < nThreads; i++) { + new Thread(task).start(); + } + + latch.await(); + + assertEquals(nThreads, leapArray.currentWindow(time).value().pass()); + } + + @Test + public void testGetPreviousWindow() { + WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + long time = TimeUtil.currentTimeMillis(); + WindowWrap previousWindow = leapArray.currentWindow(time); + assertNull(leapArray.getPreviousWindow(time)); + + long nextTime = time + windowLengthInMs; + assertSame(previousWindow, leapArray.getPreviousWindow(nextTime)); + + long longTime = time + 11 * windowLengthInMs; + assertNull(leapArray.getPreviousWindow(longTime)); + } + + @Test + public void testListWindows() { + final int windowLengthInMs = 100; + final int intervalInSec = 1; + + WindowLeapArray leapArray = new WindowLeapArray(windowLengthInMs, intervalInSec); + long time = TimeUtil.currentTimeMillis(); + + Set> windowWraps = new HashSet>(); + + windowWraps.add(leapArray.currentWindow(time)); + windowWraps.add(leapArray.currentWindow(time + windowLengthInMs)); + + List> list = leapArray.list(); + for (WindowWrap wrap : list) { + assertTrue(windowWraps.contains(wrap)); + } + + leapArray.currentWindow(time + windowLengthInMs * 20 + intervalInSec * 1000).value().addPass(); + + assertEquals(1, leapArray.list().size()); + } +} \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java new file mode 100755 index 00000000..968cb5c4 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.degrade; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/** + * @author jialiang.linjl + */ +public class DegradeTest { + + @Test + public void testAverageRtDegrade() throws InterruptedException { + String key = "test_degrade_average_rt"; + ClusterNode cn = mock(ClusterNode.class); + ClusterBuilderSlot.getClusterNodeMap().put(new StringResourceWrapper(key, EntryType.IN), cn); + + Context context = mock(Context.class); + DefaultNode node = mock(DefaultNode.class); + when(node.getClusterNode()).thenReturn(cn); + when(cn.avgRt()).thenReturn(2L); + + DegradeRule rule = new DegradeRule(); + rule.setCount(1); + rule.setResource(key); + rule.setTimeWindow(5); + + for (int i = 0; i < 4; i++) { + assertTrue(rule.passCheck(context, node, 1)); + } + + // The third time will fail. + assertFalse(rule.passCheck(context, node, 1)); + assertFalse(rule.passCheck(context, node, 1)); + + // Restore. + TimeUnit.SECONDS.sleep(6); + assertTrue(rule.passCheck(context, node, 1)); + } + + @Test + public void testExceptionRatioModeDegrade() throws Throwable { + String key = "test_degrade_exception_ratio"; + ClusterNode cn = mock(ClusterNode.class); + when(cn.exceptionQps()).thenReturn(2L); + ClusterBuilderSlot.getClusterNodeMap().put(new StringResourceWrapper(key, EntryType.IN), cn); + + Context context = mock(Context.class); + DefaultNode node = mock(DefaultNode.class); + when(node.getClusterNode()).thenReturn(cn); + + DegradeRule rule = new DegradeRule(); + rule.setCount(0.5); + rule.setResource(key); + rule.setTimeWindow(5); + rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION); + + when(cn.successQps()).thenReturn(4L); + + // Will fail. + assertFalse(rule.passCheck(context, node, 1)); + + // Restore from the degrade timeout. + TimeUnit.SECONDS.sleep(6); + + when(cn.successQps()).thenReturn(7L); + // Will pass. + assertTrue(rule.passCheck(context, node, 1)); + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowPartialIntegrationTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowPartialIntegrationTest.java new file mode 100755 index 00000000..5da8f30b --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowPartialIntegrationTest.java @@ -0,0 +1,262 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +/** + * @author jialiang.linjl + */ +public class FlowPartialIntegrationTest { + + @Test + public void testQPSGrade() { + FlowRule flowRule = new FlowRule(); + flowRule.setResource("testQPSGrade"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(1); + FlowRuleManager.loadRules(Collections.singletonList(flowRule)); + + Entry e = null; + try { + e = SphU.entry("testQPSGrade"); + } catch (BlockException e1) { + assertTrue(false); + } + e.exit(); + + try { + e = SphU.entry("testQPSGrade"); + assertTrue(false); + } catch (BlockException e1) { + assertTrue(true); + } + } + + @Test + public void testThreadGrade() throws InterruptedException { + FlowRule flowRule = new FlowRule(); + flowRule.setResource("testThreadGrade"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); + flowRule.setCount(1); + FlowRuleManager.loadRules(Arrays.asList(flowRule)); + + final Object sequence = new Object(); + + Runnable runnable = new Runnable() { + @Override + public void run() { + Entry e = null; + try { + e = SphU.entry("testThreadGrade"); + assertTrue(true); + synchronized (sequence) { + System.out.println("notify up"); + sequence.notify(); + } + Thread.sleep(1000); + } catch (BlockException e1) { + assertTrue(false); + } catch (InterruptedException e1) { + assertTrue(false); + e1.printStackTrace(); + } + e.exit(); + } + }; + + Thread thread = new Thread(runnable); + thread.start(); + + Entry e = null; + synchronized (sequence) { + System.out.println("sleep"); + sequence.wait(); + System.out.println("wake up"); + } + try { + e = SphU.entry("testThreadGrade"); + assertTrue(false); + } catch (BlockException e1) { + assertTrue(true); + } + System.out.println("done"); + + } + + @Test + public void testOriginFlowRule() { + // normal + FlowRule flowRule = new FlowRule(); + flowRule.setResource("testOriginFlowRule"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(0); + flowRule.setLimitApp("other"); + + FlowRule flowRule2 = new FlowRule(); + flowRule2.setResource("testOriginFlowRule"); + flowRule2.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule2.setCount(1); + flowRule2.setLimitApp("app2"); + + FlowRuleManager.loadRules(Arrays.asList(flowRule, flowRule2)); + + ContextUtil.enter("node1", "app1"); + Entry e = null; + try { + e = SphU.entry("testOriginFlowRule"); + assertTrue(false); + } catch (BlockException e1) { + e1.printStackTrace(); + assertTrue(true); + } + assertTrue(e == null); + + ContextUtil.exit(); + + ContextUtil.enter("node1", "app2"); + e = null; + try { + e = SphU.entry("testOriginFlowRule"); + assertTrue(true); + } catch (BlockException e1) { + e1.printStackTrace(); + assertTrue(false); + } + + e.exit(); + ContextUtil.exit(); + } + + @Test + public void testFlowRule_other() { + + FlowRule flowRule = new FlowRule(); + flowRule.setResource("testOther"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(0); + flowRule.setLimitApp("other"); + FlowRuleManager.loadRules(Arrays.asList(flowRule)); + + Entry e = null; + try { + e = SphU.entry("testOther"); + assertTrue(true); + } catch (BlockException e1) { + e1.printStackTrace(); + assertTrue(false); + } + + if (e != null) { + assertTrue(true); + e.exit(); + } else { + assertTrue(false); + } + } + + @Test + public void testStrategy() { + + // normal + FlowRule flowRule = new FlowRule(); + flowRule.setResource("testStrategy"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(0); + flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT); + FlowRuleManager.loadRules(Arrays.asList(flowRule)); + + ContextUtil.enter("testStrategy"); + Entry e = null; + try { + e = SphU.entry("testStrategy"); + assertTrue(false); + } catch (BlockException e1) { + e1.printStackTrace(); + assertTrue(true); + } + + ContextUtil.exit(); + + flowRule = new FlowRule(); + flowRule.setResource("testStrategy"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(0); + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + flowRule.setResource("entry2"); + + FlowRuleManager.loadRules(Arrays.asList(flowRule)); + + e = null; + ContextUtil.enter("entry1"); + try { + e = SphU.entry("testStrategy"); + assertTrue(true); + } catch (BlockException e1) { + e1.printStackTrace(); + assertTrue(false); + } + e.exit(); + ContextUtil.exit(); + } + + @Test + public void testStrategy_chain() { + FlowRule flowRule = new FlowRule(); + flowRule.setResource("entry2"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(0); + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + flowRule.setRefResource("entry1"); + + FlowRuleManager.loadRules(Arrays.asList(flowRule)); + Entry e = null; + ContextUtil.enter("entry1"); + try { + e = SphU.entry("entry2"); + assertTrue(false); + } catch (BlockException e1) { + e1.printStackTrace(); + assertTrue(true); + } + + ContextUtil.exit(); + + e = null; + ContextUtil.enter("entry3"); + try { + e = SphU.entry("entry2"); + assertTrue(true); + } catch (BlockException e1) { + assertTrue(false); + } + e.exit(); + + ContextUtil.exit(); + + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleTest.java new file mode 100755 index 00000000..a2439c51 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRuleTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController; + +/** + * @author jialiang.linjl + */ +public class FlowRuleTest { + + @Test + public void testFlowRule_grade() { + + FlowRule flowRule = new FlowRule(); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(1); + flowRule.setLimitApp("default"); + flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT); + + DefaultController defaultController = new DefaultController(1, flowRule.getGrade()); + flowRule.setRater(defaultController); + + Context context = mock(Context.class); + DefaultNode node = mock(DefaultNode.class); + ClusterNode cn = mock(ClusterNode.class); + + when(context.getOrigin()).thenReturn(""); + when(node.getClusterNode()).thenReturn(cn); + when(cn.passQps()).thenReturn(1l); + + assertTrue(flowRule.passCheck(context, node, 1, new Object[0]) == false); + + flowRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); + defaultController = new DefaultController(1, flowRule.getGrade()); + flowRule.setRater(defaultController); + when(cn.curThreadNum()).thenReturn(1); + assertTrue(flowRule.passCheck(context, node, 1, new Object[0]) == false); + } + + @Test + public void testFlowRule_strategy() { + + FlowRule flowRule = new FlowRule(); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(1); + flowRule.setLimitApp("default"); + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + DefaultController defaultController = new DefaultController(1, flowRule.getGrade()); + flowRule.setRater(defaultController); + flowRule.setRefResource("entry1"); + + Context context = mock(Context.class); + DefaultNode dn = mock(DefaultNode.class); + + when(context.getName()).thenReturn("entry1"); + when(dn.passQps()).thenReturn(1l); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + + when(context.getName()).thenReturn("entry2"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + // Strategy == relate + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + ClusterNode cn = mock(ClusterNode.class); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == true); + + } + + @Test + public void testOrigin() { + FlowRule flowRule = new FlowRule(); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(1); + flowRule.setLimitApp("default"); + flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT); + DefaultController defaultController = new DefaultController(1, flowRule.getGrade()); + flowRule.setRater(defaultController); + flowRule.setRefResource("entry1"); + + Context context = mock(Context.class); + DefaultNode dn = mock(DefaultNode.class); + when(context.getOrigin()).thenReturn("origin1"); + when(dn.passQps()).thenReturn(1l); + when(context.getOriginNode()).thenReturn(dn); + + /* + * first scenario, limit app as default + * + */ + ClusterNode cn = mock(ClusterNode.class); + when(dn.getClusterNode()).thenReturn(cn); + when(cn.passQps()).thenReturn(1l); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + when(cn.passQps()).thenReturn(0l); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + flowRule.setResource("entry1"); + when(context.getName()).thenReturn("entry1"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + when(context.getName()).thenReturn("entry2"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + // relate node + flowRule.setStrategy(RuleConstant.STRATEGY_RELATE); + flowRule.setResource("worong"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + /* + * second scenario test a context with the same origin1 + * + */ + flowRule.setLimitApp("origin1"); + when(context.getName()).thenReturn("entry1"); + // direct node + flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + + // chain node + flowRule.setResource("entry1"); + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + when(context.getName()).thenReturn("entry1"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + when(context.getName()).thenReturn("entry2"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + // relate node + flowRule.setStrategy(RuleConstant.STRATEGY_RELATE); + flowRule.setResource("not exits"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + when(context.getOrigin()).thenReturn("origin2"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + + /* + * limit app= other + */ + flowRule.setLimitApp("other"); + flowRule.setResource("hello world"); + + flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + + flowRule.setStrategy(RuleConstant.STRATEGY_CHAIN); + flowRule.setResource("entry1"); + when(context.getName()).thenReturn("entry1"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0]) == false); + + when(context.getName()).thenReturn("entry2"); + assertTrue(flowRule.passCheck(context, dn, 1, new Object[0])); + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/PaceControllerTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/PaceControllerTest.java new file mode 100755 index 00000000..0fb4aaf0 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/PaceControllerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.flow.controller.PaceController; + +/** + * @author jialiang.linjl + */ +public class PaceControllerTest { + + @Test + public void testPaceController_normal() throws InterruptedException { + PaceController paceController = new PaceController(500, 10d); + Node node = mock(Node.class); + + long start = TimeUtil.currentTimeMillis(); + for (int i = 0; i < 6; i++) { + assertTrue(paceController.canPass(node, 1)); + } + long end = TimeUtil.currentTimeMillis(); + assertTrue((end - start) > 400); + } + + @Test + public void testPaceController_timeout() throws InterruptedException { + final PaceController paceController = new PaceController(500, 10d); + final Node node = mock(Node.class); + + final AtomicInteger passcount = new AtomicInteger(); + final AtomicInteger blockcount = new AtomicInteger(); + final CountDownLatch countDown = new CountDownLatch(1); + + final AtomicInteger done = new AtomicInteger(); + for (int i = 0; i < 10; i++) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + boolean pass = paceController.canPass(node, 1); + + if (pass == true) { + passcount.incrementAndGet(); + } else { + blockcount.incrementAndGet(); + } + done.incrementAndGet(); + + if (done.get() >= 10) { + countDown.countDown(); + } + } + + }, "Thread " + i); + thread.start(); + } + + countDown.await(); + System.out.println("pass:" + passcount.get()); + System.out.println("block" + blockcount.get()); + System.out.println("done" + done.get()); + assertTrue(blockcount.get() > 0); + + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpControllerTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpControllerTest.java new file mode 100755 index 00000000..340a31f9 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpControllerTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController; + +/** + * @author jialiang.linjl + */ +public class WarmUpControllerTest { + + @Test + public void testWarmUp() throws InterruptedException { + WarmUpController warmupController = new WarmUpController(10, 10, 3); + + Node node = mock(Node.class); + + when(node.passQps()).thenReturn(8L); + when(node.previousPassQps()).thenReturn(1L); + + assertFalse(warmupController.canPass(node, 1)); + + when(node.passQps()).thenReturn(1L); + when(node.previousPassQps()).thenReturn(1L); + + assertTrue(warmupController.canPass(node, 1)); + + when(node.previousPassQps()).thenReturn(10L); + + for (int i = 0; i < 100; i++) { + Thread.sleep(1000); + warmupController.canPass(node, 1); + } + when(node.passQps()).thenReturn(8L); + assertTrue(warmupController.canPass(node, 1)); + + when(node.passQps()).thenReturn(10L); + assertFalse(warmupController.canPass(node, 1)); + } +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpFlowTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpFlowTest.java new file mode 100755 index 00000000..6cda853d --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/WarmUpFlowTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.flow; + +import java.util.Arrays; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; + +/** + * @author jialiang.linjl + */ +public class WarmUpFlowTest { + + @Test + public void testWarmupFlowControl() { + FlowRule flowRule = new FlowRule(); + flowRule.setResource("testWarmupFlowControl"); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setCount(10); + flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT); + flowRule.setWarmUpPeriodSec(10); + flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); + + FlowRuleManager.loadRules(Arrays.asList(flowRule)); + + //ContextUtil.enter(null); + + //when(flowRule.selectNodeByRequsterAndStrategy(null, null, null)).thenReturn(value) + + // flowRule.passCheck(null, DefaultNode, acquireCount, args) + // when(leapArray.values()).thenReturn(new ArrayList() {{ add(windowWrap.value()); }}); + ContextUtil.enter("test"); + + ContextUtil.exit(); + + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemGuardIntegrationTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemGuardIntegrationTest.java new file mode 100755 index 00000000..16c47f8d --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemGuardIntegrationTest.java @@ -0,0 +1,23 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.system; + +/** + * @author jialiang.linjl + */ +public class SystemGuardIntegrationTest { + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemRuleTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemRuleTest.java new file mode 100755 index 00000000..f2c0a809 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/system/SystemRuleTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.block.system; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +/** + * @author jialiang.linjl + */ +public class SystemRuleTest { + + @Test + public void testSystemRule_load() { + SystemRule systemRule = new SystemRule(); + + systemRule.setAvgRt(4000L); + + SystemRuleManager.loadRules(Collections.singletonList(systemRule)); + } + + @Test + public void testSystemRule_avgRt() throws BlockException { + + SystemRule systemRule = new SystemRule(); + + systemRule.setAvgRt(4L); + + Context context = mock(Context.class); + DefaultNode node = mock(DefaultNode.class); + ClusterNode cn = mock(ClusterNode.class); + + when(context.getOrigin()).thenReturn(""); + when(node.getClusterNode()).thenReturn(cn); + + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterNodeBuilder.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterNodeBuilder.java new file mode 100755 index 00000000..baa9530a --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/clusterbuilder/ClusterNodeBuilder.java @@ -0,0 +1,66 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.clusterbuilder; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.Node; + +/** + * @author jialiang.linjl + */ +public class ClusterNodeBuilder { + + @Test + public void clusterNodeBuilder_normal() throws Exception { + ContextUtil.enter("entry1", "caller1"); + + Entry nodeA = SphU.entry("nodeA"); + + Node curNode = nodeA.getCurNode(); + assertTrue(curNode.getClass() == DefaultNode.class); + DefaultNode dN = (DefaultNode)curNode; + assertTrue(dN.getClusterNode().getOriginCountMap().containsKey("caller1")); + assertTrue(nodeA.getOriginNode() == dN.getClusterNode().getOriginNode("caller1")); + + if (nodeA != null) { + nodeA.exit(); + } + ContextUtil.exit(); + + ContextUtil.enter("entry4", "caller2"); + + nodeA = SphU.entry("nodeA"); + + curNode = nodeA.getCurNode(); + assertTrue(curNode.getClass() == DefaultNode.class); + DefaultNode dN1 = (DefaultNode)curNode; + assertTrue(dN1.getClusterNode().getOriginCountMap().containsKey("caller2")); + assertTrue(dN1 != dN); + + if (nodeA != null) { + nodeA.exit(); + } + ContextUtil.exit(); + } + +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorTest.java new file mode 100755 index 00000000..4eddcf0f --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.slots.nodeselector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; + +/** + * @author jialiang.linjl + * @author Eric Zhao + */ +public class NodeSelectorTest { + + @Test + public void testSingleEntrance() throws Exception { + final String contextName = "entry_SingleEntrance"; + ContextUtil.enter(contextName); + + EntranceNode entranceNode = null; + for (Node node : Constants.ROOT.getChildList()) { + entranceNode = (EntranceNode)node; + if (entranceNode.getId().getName().equals(contextName)) { + break; + } else { + System.out.println("Single entry: " + entranceNode.getId().getName()); + } + } + assertNotNull(entranceNode); + assertTrue(entranceNode.getId().getName().equalsIgnoreCase(contextName)); + final String resName = "nodeA"; + Entry nodeA = SphU.entry(resName); + + assertNotNull(ContextUtil.getContext().getCurNode()); + assertEquals(resName, ((DefaultNode)ContextUtil.getContext().getCurNode()).getId().getName()); + boolean hasNode = false; + for (Node node : entranceNode.getChildList()) { + if (((DefaultNode)node).getId().getName().equals(resName)) { + hasNode = true; + } + } + assertTrue(hasNode); + + if (nodeA != null) { + nodeA.exit(); + } + ContextUtil.exit(); + } + + @Test + public void testMultipleEntrance() throws Exception { + final String firstEntry = "entry_multiple_one"; + final String anotherEntry = "entry_multiple_another"; + final String resName = "nodeA"; + + Node firstNode, anotherNode; + ContextUtil.enter(firstEntry); + Entry nodeA = SphU.entry(resName); + firstNode = ContextUtil.getContext().getCurNode(); + if (nodeA != null) { + nodeA.exit(); + } + ContextUtil.exit(); + + ContextUtil.enter(anotherEntry); + nodeA = SphU.entry(resName); + anotherNode = ContextUtil.getContext().getCurNode(); + if (nodeA != null) { + nodeA.exit(); + } + + assertNotSame(firstNode, anotherNode); + + for (Node node : Constants.ROOT.getChildList()) { + EntranceNode firstEntrance = (EntranceNode)node; + if (firstEntrance.getId().getName().equals(firstEntry)) { + assertEquals(1, firstEntrance.getChildList().size()); + for (Node child : firstEntrance.getChildList()) { + assertEquals(resName, ((DefaultNode)child).getId().getName()); + } + } else if (firstEntrance.getId().getName().equals(anotherEntry)) { + assertEquals(1, firstEntrance.getChildList().size()); + for (Node child : firstEntrance.getChildList()) { + assertEquals(resName, ((DefaultNode)child).getId().getName()); + } + } else { + System.out.println("Multiple entries: " + firstEntrance.getId().getName()); + } + } + ContextUtil.exit(); + } + + //@Test + public void testMultipleLayer() throws Exception { + // TODO: fix this + ContextUtil.enter("entry1", "appA"); + + Entry nodeA = SphU.entry("nodeA"); + assertSame(ContextUtil.getContext().getCurEntry(), nodeA); + + DefaultNode dnA = (DefaultNode)nodeA.getCurNode(); + assertNotNull(dnA); + assertSame("nodeA", dnA.getId().getName()); + + Entry nodeB = SphU.entry("nodeB"); + assertSame(ContextUtil.getContext().getCurEntry(), nodeB); + DefaultNode dnB = (DefaultNode)nodeB.getCurNode(); + assertNotNull(dnB); + assertTrue(dnA.getChildList().contains(dnB)); + + Entry nodeC = SphU.entry("nodeC"); + assertSame(ContextUtil.getContext().getCurEntry(), nodeC); + DefaultNode dnC = (DefaultNode)nodeC.getCurNode(); + assertNotNull(dnC); + assertTrue(dnB.getChildList().contains(dnC)); + + if (nodeC != null) { + nodeC.exit(); + } + assertSame(ContextUtil.getContext().getCurEntry(), nodeB); + + if (nodeB != null) { + nodeB.exit(); + } + assertSame(ContextUtil.getContext().getCurEntry(), nodeA); + + if (nodeA != null) { + nodeA.exit(); + } + assertNull(ContextUtil.getContext().getCurEntry()); + ContextUtil.exit(); + + } + +} diff --git a/sentinel-dashboard/README.md b/sentinel-dashboard/README.md new file mode 100755 index 00000000..e9079384 --- /dev/null +++ b/sentinel-dashboard/README.md @@ -0,0 +1,48 @@ +# Sentinel控制台 + +## 0. 概述 + +Sentinel控制台是流量控制、熔断降级规则统一配置和管理的入口,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。在Sentinel控制台上,我们可以配置规则并实时查看流量控制效果。 + +## 1. 编译和启动 + +### 1.1 如何编译 + +使用如下命令将代码打包成一个fat jar: + +```bash +$ mvn clean package +``` + +### 1.2 如何启动 + +使用如下命令启动编译后的控制台: + +```bash +$ java -Dserver.port=8080 \ +-Dcsp.sentinel.dashboard.server=localhost:8080 \ +-Dproject.name=sentinel-dashboard \ +-jar target/sentinel-dashboard.jar +``` + +上述命令中我们指定几个JVM参数,其中`-Dserver.port=8080`用于指定spring boot启动端口为`8080`,其余几个是Sentinel客户端的参数。为便于演示,我们对控制台本身加入了流量控制功能,具体做法是引入`CommonFilter`这个Sentinel拦截器,上述JVM参数的含义是: + +| 参数 | 作用 | +|--------|--------| +|`Dcsp.sentinel.dashboard.server=localhost:8080`|向Sentinel客户端指定控制台的地址。| +|`-Dproject.name=sentinel-dashboard`|向Sentinel指定本程序名称。| + +全部配置项参考[启动配置项](https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9) + +经过上述配置,控制台启动后会自动向自己发送心跳。程序启动后浏览器访问`localhost:8080`即可访问Sentinel控制台。 + +## 2. 客户端接入 + +选择合适的方式接入Sentinel,然后在应用启动时加入JVM参数`-Dcsp.sentinel.dashboard.server=consoleIp:port`指定控制台地址和端口,Sentinel客户端会自动向控制台发送心跳包,将客户端纳入到控制台的管辖之下。 + +## 3. 验证是否接入成功 + +客户端正确配置并启动后,会主动向控制台发送心跳包,汇报自己的存在;控制台收到客户端心跳包之后,会在左侧导航栏中显示该客户端信息。控制台能够看到客户端的机器信息,则表明客户端接入成功了。 + + +更多:[控制台功能介绍](./Sentinel_Dashboard_Feature.md)。 \ No newline at end of file diff --git a/sentinel-dashboard/Sentinel_Dashboard_Feature.md b/sentinel-dashboard/Sentinel_Dashboard_Feature.md new file mode 100755 index 00000000..10e5744b --- /dev/null +++ b/sentinel-dashboard/Sentinel_Dashboard_Feature.md @@ -0,0 +1,47 @@ +# Sentinel控制台功能介绍 + +## 0. 概述 + +Sentinel控制台是流量控制、熔断降级规则统一配置和管理的入口,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。在Sentinel控制台上,我们可以配置规则并实时查看流量控制效果。使用Sentinel控制台的流程如下: + +``` +客户端接入 -> 机器自发现 -> 查看簇点链路 -> 配置流控规则 -> 查看流控效果 +``` + +## 1. 功能介绍 + +### 1.1 机器自发现 + +Sentinel提供内置的机器自发现功能,无需依赖第三方服务发现组件即可实现客户端的发现。点击Sentinel控制台左侧导航栏的“机器列表”菜单,可查看集群机器数量和机器健康状况。 + +### 1.2 簇点链路自发现 + +Sentinel将每一个需要流控的URL或者接口称为一个资源,并使用URL地址或方法签名表示资源。Sentinel会自动发现所有潜在的资源和资源之间的调用链,并在“簇点链路”页面展示。资源是设置流控规则的载体,通过簇点链路自发现,可以方便的对重要资源设置流控规则、熔断降级规则等。 + +> 注意:客户端有访问量之后,才能在簇点链路页面看到资源监控。 + +### 1.3 实时监控 + +Sentinel监控功能能够实时查看集群中每个资源的实时访问以及流控情况。控制台左侧导航栏的“实时监控”菜单对应该功能。 + +### 1.4 流控降级规则设置 + +Sentinel提供了多种规则来保护系统的不同部分。流量控制规则用于保护服务提供方,熔断降级规则用于保护服务消费方,系统保护规则用于保护整个系统。 + +#### 1.4.1 流量控制规则 + +流量控制规则用于保护服务提供方,服务提供方指任何可以对外提供服务的系统,比如向终端用户提供Web服务的Web应用,向微服务消费方提供服务的Dubbo Service Provider等。系统的服务能力是有限的,如果消费方请求速度过高,则采用相应的保护策略,或是直接拒绝,或是排队等待。通过“流控规则”页面可以查看和配置流量控制规则。 + +#### 1.4.2 熔断降级规则 + +降级规则用于保护服务消费方。在微服务架构中,业务系统通常要依赖多个服务,依赖的某个服务不可用将会影响业务系统的可用性。解决这个问题的方法是及时发现服务的“不可用”状态,在调用时快速失败而不是等待调动超时或者重试。Sentinel通过熔断降级来达到快速失败的目的。通过“降级规则”页面可以查看和配置降级规则。 + +#### 1.4.3 系统保护规则 + +系统保护规则(简称`系统规则`)用于保护整个系统指标处于安全水位。无论是服务提供方还是消费方,活跃线程数过多、系统LOAD等过高都是系统处于高水位的指标。当系统水位过高时,系统应拒绝对外提供服务以便迅速降低资源占用。通过“系统规则”页面可以查看和设置系统保护规则。 + +## 2. 限制 + +本控制台只是用于演示Sentinel的基本能力和工作流程,并没有依赖生产环境中所必需的组件,比如**持久化的后端数据库、可靠的配置中心**等。目前Sentinel采用内存态的方式存储监控和规则数据,监控最长存储时间为5分钟,控制台重启后数据丢失。 + +更多:[Sentinel控制台启动和客户端接入](./README.md)。 \ No newline at end of file diff --git a/sentinel-dashboard/pom.xml b/sentinel-dashboard/pom.xml new file mode 100755 index 00000000..bacd451c --- /dev/null +++ b/sentinel-dashboard/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-parent + 0.1.0 + + + sentinel-dashboard + 0.1.0 + jar + + + 1.8 + 1.8 + + + + + com.alibaba.csp + sentinel-core + ${project.version} + + + com.alibaba.csp + sentinel-web-servlet + ${project.version} + + + com.alibaba.csp + sentinel-transport-simple-http + ${project.version} + + + com.alibaba.csp + sentinel-transport-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + 1.5.9.RELEASE + + + org.springframework.boot + spring-boot-starter-web + 1.5.9.RELEASE + + + org.springframework.boot + spring-boot-starter-logging + 1.5.9.RELEASE + + + org.springframework.boot + spring-boot-starter-tomcat + 1.5.9.RELEASE + + + + org.springframework.boot + spring-boot-devtools + 1.5.9.RELEASE + true + + + org.springframework.boot + spring-boot-starter-test + 1.5.9.RELEASE + test + + + + log4j + log4j + 1.2.14 + + + + commons-lang + commons-lang + 2.6 + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + org.apache.httpcomponents + httpcore + 4.4.5 + + + org.apache.httpcomponents + httpasyncclient + 4.1.3 + + + org.apache.httpcomponents + httpcore-nio + 4.4.6 + + + com.alibaba + fastjson + 1.2.47 + + + + + + sentinel-dashboard + + + org.springframework.boot + spring-boot-maven-plugin + + true + com.taobao.csp.sentinel.dashboard.Application + + + + + repackage + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + src/main/resources + + + + src/main/webapp/ + + resources/node_modules/** + + + + + + diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/Application.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/Application.java new file mode 100755 index 00000000..80d2f3c0 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/Application.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.support.SpringBootServletInitializer; + +@SpringBootApplication +public class Application extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(Application.class); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/config/WebConfig.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/config/WebConfig.java new file mode 100755 index 00000000..ba9b095d --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/config/WebConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.config; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * @author leyou + */ +@Configuration +public class WebConfig extends WebMvcConfigurerAdapter { + private static Logger logger = LoggerFactory.getLogger(WebConfig.class); + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/"); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("forward:/index.htm"); + } + + /** + * Add {@link CommonFilter} to the server, this is the simplest way to use Sentinel + * for Web application. + * + * @return + */ + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + logger.info("sentinelFilterRegistration(), add CommonFilter"); + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.addInitParameter("paramName", "paramValue"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + + return registration; + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java new file mode 100755 index 00000000..19dc4a3f --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java @@ -0,0 +1,99 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +import com.taobao.csp.sentinel.dashboard.discovery.AppInfo; + +/** + * @author leyou + */ +public class ApplicationEntity { + private Long id; + private Date gmtCreate; + private Date gmtModified; + private String app; + private String activeConsole; + private Date lastFetch; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getActiveConsole() { + return activeConsole; + } + + public Date getLastFetch() { + return lastFetch; + } + + public void setLastFetch(Date lastFetch) { + this.lastFetch = lastFetch; + } + + public void setActiveConsole(String activeConsole) { + this.activeConsole = activeConsole; + } + + public AppInfo toAppInfo() { + AppInfo appInfo = new AppInfo(); + appInfo.setApp(app); + + return appInfo; + } + + @Override + public String toString() { + return "ApplicationEntity{" + + "id=" + id + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", app='" + app + '\'' + + ", activeConsole='" + activeConsole + '\'' + + ", lastFetch=" + lastFetch + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/DegradeRuleEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/DegradeRuleEntity.java new file mode 100755 index 00000000..bc56742c --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/DegradeRuleEntity.java @@ -0,0 +1,157 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; + +/** + * @author leyou + */ +public class DegradeRuleEntity implements RuleEntity { + private Long id; + private String app; + private String ip; + private Integer port; + private String resource; + private String limitApp; + private Double count; + private Integer timeWindow; + /** + * 0 rt 限流; 1为异常; + */ + private Integer grade; + private Date gmtCreate; + private Date gmtModified; + + public static DegradeRuleEntity fromDegradeRule(String app, String ip, Integer port, DegradeRule rule) { + DegradeRuleEntity entity = new DegradeRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setResource(rule.getResource()); + entity.setLimitApp(rule.getLimitApp()); + entity.setCount(rule.getCount()); + entity.setTimeWindow(rule.getTimeWindow()); + entity.setGrade(rule.getGrade()); + return entity; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getLimitApp() { + return limitApp; + } + + public void setLimitApp(String limitApp) { + this.limitApp = limitApp; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Integer getTimeWindow() { + return timeWindow; + } + + public void setTimeWindow(Integer timeWindow) { + this.timeWindow = timeWindow; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public DegradeRule toDegradeRule() { + DegradeRule rule = new DegradeRule(); + rule.setResource(resource); + rule.setLimitApp(limitApp); + rule.setCount(count); + rule.setTimeWindow(timeWindow); + rule.setGrade(grade); + return rule; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/FlowRuleEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/FlowRuleEntity.java new file mode 100755 index 00000000..aa5a1238 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/FlowRuleEntity.java @@ -0,0 +1,218 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; + +/** + * @author leyou + */ +public class FlowRuleEntity implements RuleEntity { + + private Long id; + private String app; + private String ip; + private Integer port; + private String limitApp; + private String resource; + /** + * 0为线程数;1为qps + */ + private Integer grade; + private Double count; + /** + * 0为直接限流;1为关联限流;2为链路限流 + ***/ + private Integer strategy; + private String refResource; + /** + * 0. default, 1. warm up, 2. rate limiter + */ + private Integer controlBehavior; + private Integer warmUpPeriodSec; + /** + * max queueing time in rate limiter behavior + */ + private Integer maxQueueingTimeMs; + private Date gmtCreate; + private Date gmtModified; + + public static FlowRuleEntity fromFlowRule(String app, String ip, Integer port, FlowRule rule) { + FlowRuleEntity entity = new FlowRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setLimitApp(rule.getLimitApp()); + entity.setResource(rule.getResource()); + entity.setGrade(rule.getGrade()); + entity.setCount(rule.getCount()); + entity.setStrategy(rule.getStrategy()); + entity.setRefResource(rule.getRefResource()); + entity.setControlBehavior(rule.getControlBehavior()); + entity.setWarmUpPeriodSec(rule.getWarmUpPeriodSec()); + entity.setMaxQueueingTimeMs(rule.getMaxQueueingTimeMs()); + return entity; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public String getLimitApp() { + return limitApp; + } + + public void setLimitApp(String limitApp) { + this.limitApp = limitApp; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Integer getStrategy() { + return strategy; + } + + public void setStrategy(Integer strategy) { + this.strategy = strategy; + } + + public String getRefResource() { + return refResource; + } + + public void setRefResource(String refResource) { + this.refResource = refResource; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getWarmUpPeriodSec() { + return warmUpPeriodSec; + } + + public void setWarmUpPeriodSec(Integer warmUpPeriodSec) { + this.warmUpPeriodSec = warmUpPeriodSec; + } + + public Integer getMaxQueueingTimeMs() { + return maxQueueingTimeMs; + } + + public void setMaxQueueingTimeMs(Integer maxQueueingTimeMs) { + this.maxQueueingTimeMs = maxQueueingTimeMs; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public FlowRule toFlowRule() { + FlowRule flowRule = new FlowRule(); + flowRule.setCount(this.count); + flowRule.setGrade(this.grade); + flowRule.setResource(this.resource); + flowRule.setLimitApp(this.limitApp); + flowRule.setRefResource(this.refResource); + flowRule.setStrategy(this.strategy); + if (this.controlBehavior != null) { + flowRule.setControlBehavior(controlBehavior); + } + if (this.warmUpPeriodSec != null) { + flowRule.setWarmUpPeriodSec(warmUpPeriodSec); + } + if (this.maxQueueingTimeMs != null) { + flowRule.setMaxQueueingTimeMs(maxQueueingTimeMs); + } + return flowRule; + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MachineEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MachineEntity.java new file mode 100755 index 00000000..f0cba232 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MachineEntity.java @@ -0,0 +1,124 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; + +/** + * @author leyou + */ +public class MachineEntity { + private Long id; + private Date gmtCreate; + private Date gmtModified; + private String app; + private String ip; + private String hostname; + private Date timestamp; + private Integer port; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public MachineInfo toMachineInfo() { + MachineInfo machineInfo = new MachineInfo(); + + machineInfo.setApp(app); + machineInfo.setHostname(hostname); + machineInfo.setIp(ip); + machineInfo.setPort(port); + machineInfo.setVersion(timestamp); + + return machineInfo; + } + + @Override + public String toString() { + return "MachineEntity{" + + "id=" + id + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", app='" + app + '\'' + + ", ip='" + ip + '\'' + + ", hostname='" + hostname + '\'' + + ", timestamp=" + timestamp + + ", port=" + port + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricEntity.java new file mode 100755 index 00000000..c8c2806a --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricEntity.java @@ -0,0 +1,223 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +/** + * @author leyou + */ +public class MetricEntity { + private Long id; + private Date gmtCreate; + private Date gmtModified; + private String app; + /** + * 监控信息的时间戳 + */ + private Date timestamp; + private String resource; + private Long passedQps; + private Long successQps; + private Long blockedQps; + /** + * 发生异常的次数 + */ + private Long exception; + + /** + * 所有successQps的Rt的和。 + */ + private double rt; + + /** + * 本次聚合的总条数 + */ + private int count; + + private int resourceCode; + + public static MetricEntity copyOf(MetricEntity oldEntity) { + MetricEntity entity = new MetricEntity(); + entity.setId(oldEntity.getId()); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(oldEntity.getGmtModified()); + entity.setApp(oldEntity.getApp()); + entity.setTimestamp(oldEntity.getTimestamp()); + entity.setResource(oldEntity.getResource()); + entity.setPassedQps(oldEntity.getPassedQps()); + entity.setBlockedQps(oldEntity.getBlockedQps()); + entity.setSuccessQps(oldEntity.getSuccessQps()); + entity.setException(oldEntity.getException()); + entity.setRt(oldEntity.getRt()); + entity.setCount(oldEntity.getCount()); + entity.setResource(oldEntity.getResource()); + return entity; + } + + public synchronized void addPassedQps(Long passedQps) { + this.passedQps += passedQps; + } + + public synchronized void addBlockedQps(Long blockedQps) { + this.blockedQps += blockedQps; + } + + public synchronized void addException(Long exception) { + this.exception += exception; + } + + public synchronized void addCount(int count) { + this.count += count; + } + + public synchronized void addRtAndSuccessQps(double avgRt, Long successQps) { + this.rt += avgRt * successQps; + this.successQps += successQps; + } + + /** + * {@link #rt} = {@code avgRt * successQps} + * + * @param avgRt average rt of {@code successQps} + * @param successQps + */ + public synchronized void setRtAndSuccessQps(double avgRt, Long successQps) { + this.rt = avgRt * successQps; + this.successQps = successQps; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + this.resourceCode = resource.hashCode(); + } + + public Long getPassedQps() { + return passedQps; + } + + public void setPassedQps(Long passedQps) { + this.passedQps = passedQps; + } + + public Long getBlockedQps() { + return blockedQps; + } + + public void setBlockedQps(Long blockedQps) { + this.blockedQps = blockedQps; + } + + public Long getException() { + return exception; + } + + public void setException(Long exception) { + this.exception = exception; + } + + public double getRt() { + return rt; + } + + public void setRt(double rt) { + this.rt = rt; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public int getResourceCode() { + return resourceCode; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + @Override + public String toString() { + return "MetricEntity{" + + "id=" + id + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", app='" + app + '\'' + + ", timestamp=" + timestamp + + ", resource='" + resource + '\'' + + ", passedQps=" + passedQps + + ", blockedQps=" + blockedQps + + ", successQps=" + successQps + + ", exception=" + exception + + ", rt=" + rt + + ", count=" + count + + ", resourceCode=" + resourceCode + + '}'; + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java new file mode 100755 index 00000000..47fd7d33 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/MetricPositionEntity.java @@ -0,0 +1,121 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +/** + * @author leyou + */ +public class MetricPositionEntity { + private long id; + private Date gmtCreate; + private Date gmtModified; + private String app; + private String ip; + /** + * Sentinel在该应用上使用的端口 + */ + private int port; + + /** + * 机器名,冗余字段 + */ + private String hostname; + + /** + * 上一次拉取的最晚时间戳 + */ + private Date lastFetch; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public Date getLastFetch() { + return lastFetch; + } + + public void setLastFetch(Date lastFetch) { + this.lastFetch = lastFetch; + } + + @Override + public String toString() { + return "MetricPositionEntity{" + + "id=" + id + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", app='" + app + '\'' + + ", ip='" + ip + '\'' + + ", port=" + port + + ", hostname='" + hostname + '\'' + + ", lastFetch=" + lastFetch + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/RuleEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/RuleEntity.java new file mode 100755 index 00000000..2375f1fc --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/RuleEntity.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +/** + * @author leyou + */ +public interface RuleEntity { + + Long getId(); + + void setId(Long id); + + String getApp(); + + String getIp(); + + Integer getPort(); + + Date getGmtCreate(); +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/SystemRuleEntity.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/SystemRuleEntity.java new file mode 100755 index 00000000..8041eda2 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/datasource/entity/SystemRuleEntity.java @@ -0,0 +1,146 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.datasource.entity; + +import java.util.Date; + +import com.alibaba.csp.sentinel.slots.system.SystemRule; + +/** + * @author leyou + */ +public class SystemRuleEntity implements RuleEntity { + + private Long id; + + private String app; + private String ip; + private Integer port; + private Double avgLoad; + private Long avgRt; + private Long maxThread; + private Double qps; + + private Date gmtCreate; + private Date gmtModified; + + public static SystemRuleEntity fromSystemRule(String app, String ip, Integer port, SystemRule rule) { + SystemRuleEntity entity = new SystemRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setAvgLoad(rule.getHighestSystemLoad()); + entity.setAvgRt(rule.getAvgRt()); + entity.setMaxThread(rule.getMaxThread()); + entity.setQps(rule.getQps()); + return entity; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Double getAvgLoad() { + return avgLoad; + } + + public void setAvgLoad(Double avgLoad) { + this.avgLoad = avgLoad; + } + + public Long getAvgRt() { + return avgRt; + } + + public void setAvgRt(Long avgRt) { + this.avgRt = avgRt; + } + + public Long getMaxThread() { + return maxThread; + } + + public void setMaxThread(Long maxThread) { + this.maxThread = maxThread; + } + + public Double getQps() { + return qps; + } + + public void setQps(Double qps) { + this.qps = qps; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public SystemRule toSystemRule() { + SystemRule rule = new SystemRule(); + rule.setHighestSystemLoad(avgLoad); + rule.setAvgRt(avgRt); + rule.setMaxThread(maxThread); + rule.setQps(qps); + return rule; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppInfo.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppInfo.java new file mode 100755 index 00000000..402d7c1a --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppInfo.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.discovery; + +import java.util.Set; +import java.util.TreeSet; + +public class AppInfo { + + private String app = ""; + + private Set machines = new TreeSet(); + + public AppInfo() { + } + + public AppInfo(String app) { + this.app = app; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public synchronized Set getMachines() { + return machines; + } + + @Override + public String toString() { + return "AppInfo{" + "app='" + app + ", machines=" + machines + '}'; + } + + public synchronized boolean addMachine(MachineInfo machineInfo) { + machines.remove(machineInfo); + return machines.add(machineInfo); + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppManagement.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppManagement.java new file mode 100755 index 00000000..8002c659 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/AppManagement.java @@ -0,0 +1,66 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.discovery; + +import java.util.List; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class AppManagement implements MachineDiscovery { + + //@Value("${appmanagement.maxnode}") + //private Integer maxNode; + // + //@Value("${discovery.type}") + //private String type; + + @Autowired + private ApplicationContext context; + + MachineDiscovery machineDiscovery; + + @PostConstruct + public void init() { + machineDiscovery = context.getBean(SimpleMachineDiscovery.class); + } + + @Override + public Set getBriefApps() { + return machineDiscovery.getBriefApps(); + } + + @Override + public long addMachine(MachineInfo machineInfo) { + return machineDiscovery.addMachine(machineInfo); + } + + @Override + public List getAppNames() { + return machineDiscovery.getAppNames(); + } + + @Override + public AppInfo getDetailApp(String app) { + return machineDiscovery.getDetailApp(app); + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineDiscovery.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineDiscovery.java new file mode 100755 index 00000000..989128bd --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineDiscovery.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.discovery; + +import java.util.List; +import java.util.Set; + +public interface MachineDiscovery { + + long MAX_CLIENT_LIVE_TIME_MS = 1000 * 60 * 5; + String UNKNOWN_APP_NAME = "UNKNOWN"; + + List getAppNames(); + + Set getBriefApps(); + + AppInfo getDetailApp(String app); + + long addMachine(MachineInfo machineInfo); +} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineInfo.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineInfo.java new file mode 100755 index 00000000..b3aea6e2 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/MachineInfo.java @@ -0,0 +1,129 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.discovery; + +import java.util.Date; + +import com.alibaba.csp.sentinel.util.StringUtil; + +public class MachineInfo implements Comparable { + private String app = ""; + private String hostname = ""; + private String ip = ""; + private Integer port = -1; + private Date version; + + public static MachineInfo of(String app, String ip, Integer port) { + MachineInfo machineInfo = new MachineInfo(); + machineInfo.setApp(app); + machineInfo.setIp(ip); + machineInfo.setPort(port); + return machineInfo; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Date getVersion() { + return version; + } + + public void setVersion(Date version) { + this.version = version; + } + + @Override + public int compareTo(MachineInfo o) { + if (this == o) { + return 0; + } + if (!port.equals(o.getPort())) { + return port.compareTo(o.getPort()); + } + if (!StringUtil.equals(app, o.getApp())) { + return app.compareToIgnoreCase(o.getApp()); + } + return ip.compareToIgnoreCase(o.getIp()); + } + + @Override + public String toString() { + return "MachineInfo{" + + "app='" + app + '\'' + + ", hostname='" + hostname + '\'' + + ", ip='" + ip + '\'' + + ", port=" + port + + ", version=" + version + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MachineInfo)) { + return false; + } + + MachineInfo that = (MachineInfo)o; + + if (app != null ? !app.equals(that.app) : that.app != null) { + return false; + } + if (hostname != null ? !hostname.equals(that.hostname) : that.hostname != null) { + return false; + } + return ip != null ? ip.equals(that.ip) : that.ip == null; + } + + @Override + public int hashCode() { + int result = app != null ? app.hashCode() : 0; + result = 31 * result + (hostname != null ? hostname.hashCode() : 0); + result = 31 * result + (ip != null ? ip.hashCode() : 0); + return result; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java new file mode 100755 index 00000000..1148664f --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.discovery; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +/** + * @author leyou + */ +@Component +public class SimpleMachineDiscovery implements MachineDiscovery { + protected ConcurrentHashMap apps = new ConcurrentHashMap<>(); + + @Override + public long addMachine(MachineInfo machineInfo) { + AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), app -> new AppInfo(app)); + appInfo.addMachine(machineInfo); + return 1; + } + + @Override + public List getAppNames() { + return new ArrayList<>(apps.keySet()); + } + + @Override + public AppInfo getDetailApp(String app) { + return apps.get(app); + } + + @Override + public Set getBriefApps() { + return new HashSet<>(apps.values()); + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/domain/ResourceTreeNode.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/domain/ResourceTreeNode.java new file mode 100755 index 00000000..542a0814 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/domain/ResourceTreeNode.java @@ -0,0 +1,242 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.domain; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.command.vo.NodeVo; + +/** + * @author leyou + */ +public class ResourceTreeNode { + private String id; + private String parentId; + private String resource; + + private Integer threadNum; + private Long passQps; + private Long blockedQps; + private Long totalQps; + private Long averageRt; + private Long successQps; + private Long exceptionQps; + private Long oneMinutePassed; + private Long oneMinuteBlocked; + private Long oneMinuteException; + private Long oneMinuteTotal; + + private boolean visible = true; + + private List children = new ArrayList<>(); + + public static ResourceTreeNode fromNodeVoList(List nodeVos) { + if (nodeVos == null || nodeVos.isEmpty()) { + return null; + } + ResourceTreeNode root = null; + Map map = new HashMap<>(); + for (NodeVo vo : nodeVos) { + ResourceTreeNode node = fromNodeVo(vo); + map.put(node.id, node); + // real root + if (node.parentId == null) { + root = node; + } else if (map.containsKey(node.parentId)) { + map.get(node.parentId).children.add(node); + } else { + // impossible + } + } + return root; + } + + public static ResourceTreeNode fromNodeVo(NodeVo vo) { + ResourceTreeNode node = new ResourceTreeNode(); + node.id = vo.getId(); + node.parentId = vo.getParentId(); + node.resource = vo.getResource(); + node.threadNum = vo.getThreadNum(); + node.passQps = vo.getPassQps(); + node.blockedQps = vo.getBlockedQps(); + node.totalQps = vo.getTotalQps(); + node.averageRt = vo.getAverageRt(); + node.successQps = vo.getSuccessQps(); + node.exceptionQps = vo.getExceptionQps(); + node.oneMinutePassed = vo.getOneMinutePassed(); + node.oneMinuteBlocked = vo.getOneMinuteBlocked(); + node.oneMinuteException = vo.getOneMinuteException(); + node.oneMinuteTotal = vo.getOneMinuteTotal(); + return node; + } + + public void searchIgnoreCase(String searchKey) { + search(this, searchKey); + } + + /** + * This node is visible only when searchKey matches this.resource or at least + * one of this's children is visible + */ + private boolean search(ResourceTreeNode node, String searchKey) { + // empty matches all + if (searchKey == null || searchKey.isEmpty() || + node.resource.toLowerCase().contains(searchKey.toLowerCase())) { + node.visible = true; + } else { + node.visible = false; + } + + boolean found = false; + for (ResourceTreeNode c : node.children) { + found |= search(c, searchKey); + } + node.visible |= found; + return node.visible; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getThreadNum() { + return threadNum; + } + + public void setThreadNum(Integer threadNum) { + this.threadNum = threadNum; + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockedQps() { + return blockedQps; + } + + public void setBlockedQps(Long blockedQps) { + this.blockedQps = blockedQps; + } + + public Long getTotalQps() { + return totalQps; + } + + public void setTotalQps(Long totalQps) { + this.totalQps = totalQps; + } + + public Long getAverageRt() { + return averageRt; + } + + public void setAverageRt(Long averageRt) { + this.averageRt = averageRt; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public Long getOneMinutePassed() { + return oneMinutePassed; + } + + public void setOneMinutePassed(Long oneMinutePassed) { + this.oneMinutePassed = oneMinutePassed; + } + + public Long getOneMinuteBlocked() { + return oneMinuteBlocked; + } + + public void setOneMinuteBlocked(Long oneMinuteBlocked) { + this.oneMinuteBlocked = oneMinuteBlocked; + } + + public Long getOneMinuteException() { + return oneMinuteException; + } + + public void setOneMinuteException(Long oneMinuteException) { + this.oneMinuteException = oneMinuteException; + } + + public Long getOneMinuteTotal() { + return oneMinuteTotal; + } + + public void setOneMinuteTotal(Long oneMinuteTotal) { + this.oneMinuteTotal = oneMinuteTotal; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} + diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/HttpHelper.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/HttpHelper.java new file mode 100755 index 00000000..bde4ebfb --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/HttpHelper.java @@ -0,0 +1,327 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.fastjson.JSON; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.DegradeRuleEntity; +import com.taobao.csp.sentinel.dashboard.datasource.entity.FlowRuleEntity; +import com.taobao.csp.sentinel.dashboard.datasource.entity.SystemRuleEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Communicate with Sentinel client. + * + * @author leyou + */ +@Component +public class HttpHelper { + + private static Logger logger = LoggerFactory.getLogger(HttpHelper.class); + private static final Charset defaultCharset = Charset.forName(SentinelConfig.charset()); + + private CloseableHttpAsyncClient httpclient; + private final String resourceUrlPath = "jsonTree"; + private final String clusterNodePath = "clusterNode"; + + private final String getRulesPath = "getRules"; + private final String setRulesPath = "setRules"; + private final String flowRuleType = "flow"; + private final String degradeRuleType = "degrade"; + private final String systemRuleType = "system"; + + public HttpHelper() { + IOReactorConfig ioConfig = IOReactorConfig.custom().setConnectTimeout(3000).setSoTimeout(3000) + .setIoThreadCount(Runtime.getRuntime().availableProcessors() * 2).build(); + httpclient = HttpAsyncClients.custom().setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(final String method) { + return false; + } + }).setMaxConnTotal(4000).setMaxConnPerRoute(1000).setDefaultIOReactorConfig(ioConfig).build(); + httpclient.start(); + } + + public List fetchResourceOfMachine(String ip, int port, String type) { + String url = "http://" + ip + ":" + port + "/" + resourceUrlPath + "?type=" + type; + String body = httpGetContent(url); + if (body == null) { + return null; + } + try { + return JSON.parseArray(body, NodeVo.class); + } catch (Exception e) { + logger.info("parse ResourceOfMachine error", e); + return null; + } + } + + /** + * Fetch cluster node. + * + * @param ip ip to fetch + * @param port port of the ip + * @param includeZero whether zero value should in the result list. + * @return + */ + public List fetchClusterNodeOfMachine(String ip, int port, boolean includeZero) { + String type = "noZero"; + if (includeZero) { + type = "zero"; + } + String url = "http://" + ip + ":" + port + "/" + clusterNodePath + "?type=" + type; + String body = httpGetContent(url); + if (body == null) { + return null; + } + try { + return JSON.parseArray(body, NodeVo.class); + } catch (Exception e) { + logger.info("parse ClusterNodeOfMachine error", e); + return null; + } + } + + public List fetchFlowRuleOfMachine(String app, String ip, int port) { + String url = "http://" + ip + ":" + port + "/" + getRulesPath + "?type=" + flowRuleType; + String body = httpGetContent(url); + logger.info("FlowRule Body:{}", body); + List rules = parseFlowRule(body); + if (rules != null) { + return rules.stream().map(rule -> FlowRuleEntity.fromFlowRule(app, ip, port, rule)) + .collect(Collectors.toList()); + } else { + return null; + } + } + + public List fetchDegradeRuleOfMachine(String app, String ip, int port) { + String url = "http://" + ip + ":" + port + "/" + getRulesPath + "?type=" + degradeRuleType; + String body = httpGetContent(url); + logger.info("Degrade Body:{}", body); + List rules = parseDegradeRule(body); + if (rules != null) { + return rules.stream().map(rule -> DegradeRuleEntity.fromDegradeRule(app, ip, port, rule)) + .collect(Collectors.toList()); + } else { + return null; + } + } + + public List fetchSystemRuleOfMachine(String app, String ip, int port) { + String url = "http://" + ip + ":" + port + "/" + getRulesPath + "?type=" + systemRuleType; + String body = httpGetContent(url); + logger.info("SystemRule Body:{}", body); + List rules = parseSystemRule(body); + if (rules != null) { + return rules.stream().map(rule -> SystemRuleEntity.fromSystemRule(app, ip, port, rule)) + .collect(Collectors.toList()); + } else { + return null; + } + } + + /** + * set rules of the machine. rules == null will return immediately; + * rules.isEmpty() means setting the rules to empty. + * + * @param app + * @param ip + * @param port + * @param rules + * @return whether successfully set the rules. + */ + public boolean setFlowRuleOfMachine(String app, String ip, int port, List rules) { + if (rules == null) { + return true; + } + if (ip == null) { + throw new IllegalArgumentException("ip is null"); + } + String data = JSON.toJSONString(rules.stream().map(FlowRuleEntity::toFlowRule).collect(Collectors.toList())); + try { + data = URLEncoder.encode(data, defaultCharset.name()); + } catch (UnsupportedEncodingException e) { + logger.info("encode rule error", e); + return false; + } + String url = "http://" + ip + ":" + port + "/" + setRulesPath + "?type=" + flowRuleType + "&data=" + data; + String result = httpGetContent(url); + logger.info("setFlowRule: " + result); + return true; + } + + /** + * set rules of the machine. rules == null will return immediately; + * rules.isEmpty() means setting the rules to empty. + * + * @param app + * @param ip + * @param port + * @param rules + * @return whether successfully set the rules. + */ + public boolean setDegradeRuleOfMachine(String app, String ip, int port, List rules) { + if (rules == null) { + return true; + } + if (ip == null) { + throw new IllegalArgumentException("ip is null"); + } + String data = JSON.toJSONString( + rules.stream().map(DegradeRuleEntity::toDegradeRule).collect(Collectors.toList())); + try { + data = URLEncoder.encode(data, defaultCharset.name()); + } catch (UnsupportedEncodingException e) { + logger.info("encode rule error", e); + return false; + } + String url = "http://" + ip + ":" + port + "/" + setRulesPath + "?type=" + degradeRuleType + "&data=" + data; + String result = httpGetContent(url); + logger.info("setDegradeRule: " + result); + return true; + } + + /** + * set rules of the machine. rules == null will return immediately; + * rules.isEmpty() means setting the rules to empty. + * + * @param app + * @param ip + * @param port + * @param rules + * @return whether successfully set the rules. + */ + public boolean setSystemRuleOfMachine(String app, String ip, int port, List rules) { + if (rules == null) { + return true; + } + if (ip == null) { + throw new IllegalArgumentException("ip is null"); + } + String data = JSON.toJSONString( + rules.stream().map(SystemRuleEntity::toSystemRule).collect(Collectors.toList())); + try { + data = URLEncoder.encode(data, defaultCharset.name()); + } catch (UnsupportedEncodingException e) { + logger.info("encode rule error", e); + return false; + } + String url = "http://" + ip + ":" + port + "/" + setRulesPath + "?type=" + systemRuleType + "&data=" + data; + String result = httpGetContent(url); + logger.info("setSystemRule: " + result); + return true; + } + + private List parseFlowRule(String body) { + try { + return JSON.parseArray(body, FlowRule.class); + } catch (Exception e) { + logger.info("parser FlowRule error: ", e); + return null; + } + } + + private List parseDegradeRule(String body) { + try { + return JSON.parseArray(body, DegradeRule.class); + } catch (Exception e) { + logger.info("parser DegradeRule error: ", e); + return null; + } + } + + private List parseSystemRule(String body) { + try { + return JSON.parseArray(body, SystemRule.class); + } catch (Exception e) { + logger.info("parser SystemRule error: ", e); + return null; + } + } + + private String httpGetContent(String url) { + final HttpGet httpGet = new HttpGet(url); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference reference = new AtomicReference<>(); + httpclient.execute(httpGet, new FutureCallback() { + @Override + public void completed(final HttpResponse response) { + try { + reference.set(getBody(response)); + } catch (Exception e) { + logger.info("httpGetContent " + url + " error:", e); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + logger.info("httpGetContent " + url + " failed:", ex); + } + + @Override + public void cancelled() { + latch.countDown(); + } + }); + try { + latch.await(5, TimeUnit.SECONDS); + } catch (Exception e) { + logger.info("wait http client error:", e); + } + return reference.get(); + } + + private String getBody(HttpResponse response) throws Exception { + Charset charset = null; + try { + String contentTypeStr = response.getFirstHeader("Content-type").getValue(); + ContentType contentType = ContentType.parse(contentTypeStr); + charset = contentType.getCharset(); + } catch (Exception ignore) { + } + return EntityUtils.toString(response.getEntity(), charset != null ? charset : defaultCharset); + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemDegradeRuleStore.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemDegradeRuleStore.java new file mode 100755 index 00000000..549c47ae --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemDegradeRuleStore.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.util.concurrent.atomic.AtomicLong; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.DegradeRuleEntity; +import org.springframework.stereotype.Component; + +/** + * @author leyou + */ +@Component +public class InMemDegradeRuleStore extends InMemRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemFlowRuleStore.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemFlowRuleStore.java new file mode 100755 index 00000000..220d64ef --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemFlowRuleStore.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.util.concurrent.atomic.AtomicLong; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.FlowRuleEntity; +import org.springframework.stereotype.Component; + +/** + * Store {@link FlowRuleEntity} in memory. + * + * @author leyou + */ +@Component +public class InMemFlowRuleStore extends InMemRepositoryAdapter { + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemMetricStore.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemMetricStore.java new file mode 100755 index 00000000..59409f58 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemMetricStore.java @@ -0,0 +1,129 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.MetricEntity; +import org.springframework.stereotype.Component; + +/** + * Store metrics in memory. + * + * @author leyou + */ +@Component +public class InMemMetricStore { + public static final long MAX_METRIC_LIVE_TIME_MS = 1000 * 60 * 5; + /** + * {@code app -> resource -> timestamp -> metric} + */ + private Map>> allMetrics = new ConcurrentHashMap<>(); + + /** + * Save all metrics in memory. Metric older than {@link #MAX_METRIC_LIVE_TIME_MS} will be removed. + * + * @param metrics metrics to be saved. + */ + public synchronized void saveAll(Iterable metrics) { + if (metrics == null) { + return; + } + for (MetricEntity entity : metrics) { + allMetrics.computeIfAbsent(entity.getApp(), e -> new HashMap<>(16)) + .computeIfAbsent(entity.getResource(), e -> new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return eldest.getKey() < System.currentTimeMillis() - MAX_METRIC_LIVE_TIME_MS; + } + }).put(entity.getTimestamp().getTime(), entity); + } + } + + public synchronized List queryByAppAndResouce(String app, + String resource, + long startTime, + long endTime) { + List results = new ArrayList<>(); + Map> resouceMap = allMetrics.get(app); + if (resouceMap == null) { + return results; + } + LinkedHashMap metricsMap = resouceMap.get(resource); + if (metricsMap == null) { + return results; + } + for (Map.Entry entry : metricsMap.entrySet()) { + if (entry.getKey() >= startTime && entry.getKey() <= endTime) { + results.add(entry.getValue()); + } + } + return results; + } + + /** + * Find resources of App order by last minute b_qps desc + * + * @param app app name + * @return Resources list, order by last minute b_qps desc. + */ + public synchronized List findResourcesOfApp(String app) { + List results = new ArrayList<>(); + // resource -> timestamp -> metric + Map> resourceMap = allMetrics.get(app); + if (resourceMap == null) { + return results; + } + final long minTimeMs = System.currentTimeMillis() - 1000 * 60; + Map resourceCount = new HashMap<>(32); + + for (Map.Entry> resourceMetrics : resourceMap.entrySet()) { + for (Map.Entry metrics : resourceMetrics.getValue().entrySet()) { + if (metrics.getKey() < minTimeMs) { + continue; + } + MetricEntity newEntity = metrics.getValue(); + if (resourceCount.containsKey(resourceMetrics.getKey())) { + MetricEntity oldEntity = resourceCount.get(resourceMetrics.getKey()); + oldEntity.addPassedQps(newEntity.getPassedQps()); + oldEntity.addRtAndSuccessQps(newEntity.getRt(), newEntity.getSuccessQps()); + oldEntity.addBlockedQps(newEntity.getBlockedQps()); + oldEntity.addException(newEntity.getException()); + oldEntity.addCount(1); + } else { + resourceCount.put(resourceMetrics.getKey(), MetricEntity.copyOf(newEntity)); + } + } + } + return resourceCount.entrySet().stream().sorted((o1, o2) -> { + MetricEntity e1 = o1.getValue(); + MetricEntity e2 = o2.getValue(); + int t = e2.getBlockedQps().compareTo(e1.getBlockedQps()); + if (t != 0) { + return t; + } + return e2.getPassedQps().compareTo(e1.getPassedQps()); + }).map(e -> e.getKey()) + .collect(Collectors.toList()); + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemRepositoryAdapter.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemRepositoryAdapter.java new file mode 100755 index 00000000..bd43bea8 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemRepositoryAdapter.java @@ -0,0 +1,96 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.RuleEntity; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; + +/** + * @author leyou + */ +public abstract class InMemRepositoryAdapter implements RuleRepository { + /** + * {@code >} + */ + private Map> machineRules = new ConcurrentHashMap<>(16); + private Map allRules = new ConcurrentHashMap<>(16); + + private static final int MAX_RULES_SIZE = 10000; + + @Override + public T save(T entity) { + if (entity.getId() == null) { + entity.setId(nextId()); + } + allRules.put(entity.getId(), entity); + machineRules.computeIfAbsent(MachineInfo.of(entity.getApp(), entity.getIp(), entity.getPort()), + e -> new ConcurrentHashMap<>(32)) + .put(entity.getId(), entity); + return entity; + } + + @Override + public List saveAll(List rules) { + allRules.clear(); + machineRules.clear(); + + if (rules == null) { + return null; + } + List savedRules = new ArrayList<>(rules.size()); + for (T rule : rules) { + savedRules.add(save(rule)); + } + return savedRules; + } + + @Override + public T delete(Long id) { + T entity = allRules.remove(id); + if (entity != null) { + machineRules.get(MachineInfo.of(entity.getApp(), entity.getIp(), entity.getPort())).remove(id); + } + return entity; + } + + @Override + public T findById(Long id) { + return allRules.get(id); + } + + @Override + public List findAllByMachine(MachineInfo machineInfo) { + Map entities = machineRules.get(machineInfo); + if (entities == null) { + return new ArrayList<>(); + } + return entities.values().stream() + .collect(Collectors.toList()); + } + + /** + * Get next unused id. + * + * @return + */ + abstract protected long nextId(); +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemSystemRuleStore.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemSystemRuleStore.java new file mode 100755 index 00000000..1ed00bf3 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/InMemSystemRuleStore.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.util.concurrent.atomic.AtomicLong; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.SystemRuleEntity; +import org.springframework.stereotype.Component; + +/** + * @author leyou + */ +@Component +public class InMemSystemRuleStore extends InMemRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/RuleRepository.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/RuleRepository.java new file mode 100755 index 00000000..65892087 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/inmem/RuleRepository.java @@ -0,0 +1,75 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.inmem; + +import java.util.List; + +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; + +/** + * Interface to store and find rules. + * + * @author leyou + */ +public interface RuleRepository { + /** + * Save one. + * + * @param entity + * @return + */ + T save(T entity); + + /** + * Save all. + * + * @param rules + * @return rules saved. + */ + List saveAll(List rules); + + /** + * Delete by id + * + * @param id + * @return entity deleted + */ + T delete(ID id); + + /** + * Find by id. + * + * @param id + * @return + */ + T findById(ID id); + + /** + * Find all by machine. + * + * @param machineInfo + * @return + */ + List findAllByMachine(MachineInfo machineInfo); + + ///** + // * Find all by app and enable switch. + // * @param app + // * @param enable + // * @return + // */ + //List findAllByAppAndEnable(String app, boolean enable); +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/metric/MetricFetcher.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/metric/MetricFetcher.java new file mode 100755 index 00000000..b0345b3e --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/metric/MetricFetcher.java @@ -0,0 +1,336 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.metric; + +import java.nio.charset.Charset; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.MetricEntity; +import com.taobao.csp.sentinel.dashboard.discovery.AppManagement; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; +import com.taobao.csp.sentinel.dashboard.inmem.InMemMetricStore; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.protocol.HTTP; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Fetch metric of machines. + * + * @author leyou + */ +@Component +public class MetricFetcher { + + private static Logger logger = LoggerFactory.getLogger(MetricFetcher.class); + private static final int HTTP_OK = 200; + + public static final long MAX_CLIENT_LIVE_TIME_MS = 1000 * 60 * 5; + private static final long MAX_LAST_FETCH_INTERVAL_MS = 1000 * 15; + private static final long FETCH_INTERVAL_SECOND = 6; + private static final Charset DEFAULT_CHARSET = Charset.forName(SentinelConfig.charset()); + private final static String METRIC_URL_PATH = "metric"; + + private final long intervalSecond = 1; + + private Map appLastFetchTime = new ConcurrentHashMap<>(); + + @Autowired + private InMemMetricStore metricStore; + @Autowired + private AppManagement appManagement; + + private CloseableHttpAsyncClient httpclient; + private ScheduledExecutorService fetchScheduleService = Executors.newScheduledThreadPool(1); + private ExecutorService fetchService; + private ExecutorService fetchWorker; + + public MetricFetcher() { + int cores = Runtime.getRuntime().availableProcessors() * 2; + long keepAliveTime = 0; + int queueSize = 2048; + RejectedExecutionHandler handler = new DiscardPolicy(); + fetchService = new ThreadPoolExecutor(cores, cores, + keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize), + new NamedThreadFactory("fetchService"), handler); + fetchWorker = new ThreadPoolExecutor(cores, cores, + keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize), + new NamedThreadFactory("fetchWorker"), handler); + IOReactorConfig ioConfig = IOReactorConfig.custom() + .setConnectTimeout(3000) + .setSoTimeout(3000) + .setIoThreadCount(Runtime.getRuntime().availableProcessors() * 2) + .build(); + + httpclient = HttpAsyncClients.custom() + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(final String method) { + return false; + } + }).setMaxConnTotal(4000) + .setMaxConnPerRoute(1000) + .setDefaultIOReactorConfig(ioConfig) + .build(); + httpclient.start(); + start(); + } + + private void start() { + fetchScheduleService.scheduleAtFixedRate(() -> { + try { + fetchAllApp(); + } catch (Exception e) { + logger.info("fetchAllApp error:", e); + } + }, 10, intervalSecond, TimeUnit.SECONDS); + } + + private void writeMetric(Map map) { + if (map.isEmpty()) { + return; + } + Date date = new Date(); + for (MetricEntity entity : map.values()) { + entity.setGmtCreate(date); + entity.setGmtModified(date); + } + metricStore.saveAll(map.values()); + } + + /** + * 遍历每个APP,然后拉取该APP所有机器的metric + */ + private void fetchAllApp() { + List apps = appManagement.getAppNames(); + if (apps == null) { + return; + } + for (final String app : apps) { + fetchService.submit(() -> { + try { + doFetchAppMetric(app); + } catch (Exception e) { + logger.error("fetchAppMetric error", e); + } + }); + } + } + + /** + * fetch metric between [startTime, endTime], both side inclusive + */ + private void fetchOnce(String app, long startTime, long endTime, int maxWaitSeconds) { + if (maxWaitSeconds <= 0) { + throw new IllegalArgumentException("maxWaitSeconds must > 0, but " + maxWaitSeconds); + } + Set machines = appManagement.getDetailApp(app).getMachines(); + logger.debug("enter fetchOnce(" + app + "), machines.size()=" + machines.size() + + ", time intervalMs [" + startTime + ", " + endTime + "]"); + if (machines.isEmpty()) { + return; + } + final String msg = "fetch"; + AtomicLong dead = new AtomicLong(); + final AtomicLong success = new AtomicLong(); + final AtomicLong fail = new AtomicLong(); + + long start = System.currentTimeMillis(); + /** app_resource_timeSecond -> metric */ + final Map metricMap = new ConcurrentHashMap<>(16); + final CountDownLatch latch = new CountDownLatch(machines.size()); + for (final MachineInfo machine : machines) { + // dead + if (System.currentTimeMillis() - machine.getVersion().getTime() > MAX_CLIENT_LIVE_TIME_MS) { + latch.countDown(); + dead.incrementAndGet(); + continue; + } + final String url = "http://" + machine.getIp() + ":" + machine.getPort() + "/" + METRIC_URL_PATH + + "?startTime=" + startTime + "&endTime=" + endTime + "&refetch=" + false; + final HttpGet httpGet = new HttpGet(url); + httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE); + httpclient.execute(httpGet, new FutureCallback() { + @Override + public void completed(final HttpResponse response) { + try { + handleResponse(response, machine, metricMap); + success.incrementAndGet(); + } catch (Exception e) { + logger.error(msg + " metric " + url + " error:", e); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + fail.incrementAndGet(); + httpGet.abort(); + logger.error(msg + " metric " + url + " failed:", ex); + } + + @Override + public void cancelled() { + latch.countDown(); + fail.incrementAndGet(); + httpGet.abort(); + } + }); + } + try { + latch.await(maxWaitSeconds, TimeUnit.SECONDS); + } catch (Exception e) { + logger.info(msg + " metric, wait http client error:", e); + } + long cost = System.currentTimeMillis() - start; + //logger.info("finished " + msg + " metric for " + app + ", time intervalMs [" + startTime + ", " + endTime + // + "], total machines=" + machines.size() + ", dead=" + dead + ", fetch success=" + // + success + ", fetch fail=" + fail + ", time cost=" + cost + " ms"); + writeMetric(metricMap); + } + + private void doFetchAppMetric(final String app) { + long now = System.currentTimeMillis(); + long lastFetchMs = now - MAX_LAST_FETCH_INTERVAL_MS; + if (appLastFetchTime.containsKey(app)) { + lastFetchMs = Math.max(lastFetchMs, appLastFetchTime.get(app).get() + 1000); + } + // trim milliseconds + lastFetchMs = lastFetchMs / 1000 * 1000; + long endTime = lastFetchMs + FETCH_INTERVAL_SECOND * 1000; + if (endTime > now - 1000 * 2) { + // to near + return; + } + // update last_fetch in advance. + appLastFetchTime.computeIfAbsent(app, a -> new AtomicLong()).set(endTime); + final long finalLastFetchMs = lastFetchMs; + final long finalEndTime = endTime; + try { + // do real fetch async + fetchWorker.submit(() -> { + try { + fetchOnce(app, finalLastFetchMs, finalEndTime, 5); + } catch (Exception e) { + logger.info("fetchOnce(" + app + ") error", e); + } + }); + } catch (Exception e) { + logger.info("submit fetchOnce(" + app + ") fail, intervalMs [" + lastFetchMs + ", " + endTime + "]", e); + } + } + + private void handleResponse(final HttpResponse response, MachineInfo machine, + Map metricMap) throws Exception { + int code = response.getStatusLine().getStatusCode(); + if (code != HTTP_OK) { + return; + } + Charset charset = null; + try { + String contentTypeStr = response.getFirstHeader("Content-type").getValue(); + ContentType contentType = ContentType.parse(contentTypeStr); + charset = contentType.getCharset(); + } catch (Exception ignore) { + } + String body = EntityUtils.toString(response.getEntity(), charset != null ? charset : DEFAULT_CHARSET); + if (StringUtil.isEmpty(body)) { + //logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() + ", bodyStr is empty"); + return; + } + String[] lines = body.split("\n"); + //logger.info(machine.getApp() + ":" + machine.getIp() + ":" + machine.getPort() + + // ", bodyStr.length()=" + body.length() + ", lines=" + lines.length); + handleBody(lines, machine, metricMap); + } + + private void handleBody(String[] lines, MachineInfo machine, Map map) { + //logger.info("handleBody() lines=" + lines.length + ", machine=" + machine); + if (lines.length < 1) { + return; + } + + for (String line : lines) { + try { + MetricNode node = MetricNode.fromThinString(line); + /** + * aggregation metrics by app_resource_timeSecond, ignore ip and port. + */ + String key = buildMetricKey(machine.getApp(), node.getResource(), node.getTimestamp()); + MetricEntity entity = map.get(key); + if (entity != null) { + entity.addPassedQps(node.getPassedQps()); + entity.addBlockedQps(node.getBlockedQps()); + entity.addRtAndSuccessQps(node.getRt(), node.getSuccessQps()); + entity.addException(node.getException()); + entity.addCount(1); + } else { + entity = new MetricEntity(); + entity.setApp(machine.getApp()); + entity.setTimestamp(new Date(node.getTimestamp())); + entity.setPassedQps(node.getPassedQps()); + entity.setBlockedQps(node.getBlockedQps()); + entity.setRtAndSuccessQps(node.getRt(), node.getSuccessQps()); + entity.setException(node.getException()); + entity.setCount(1); + entity.setResource(node.getResource()); + map.put(key, entity); + } + } catch (Exception e) { + logger.info("handleBody line error: {}", line); + } + } + } + + private String buildMetricKey(String app, String resource, long timestamp) { + return app + "__" + resource + "__" + (timestamp / 1000); + } + +} + + + diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/AppController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/AppController.java new file mode 100755 index 00000000..024a094e --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/AppController.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import com.taobao.csp.sentinel.dashboard.discovery.AppInfo; +import com.taobao.csp.sentinel.dashboard.discovery.AppManagement; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; +import com.taobao.csp.sentinel.dashboard.view.vo.MachineInfoVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * 这个Controller负责app,机器信息的交互. + */ +@Controller +@RequestMapping(value = "/app", produces = MediaType.APPLICATION_JSON_VALUE) +public class AppController { + + @Autowired + AppManagement appManagement; + + @ResponseBody + @RequestMapping("/names.json") + Result> queryApps(HttpServletRequest request) { + return Result.ofSuccess(appManagement.getAppNames()); + } + + @ResponseBody + @RequestMapping("/briefinfos.json") + Result> queryAppInfos(HttpServletRequest request) { + List list = new ArrayList<>(appManagement.getBriefApps()); + Collections.sort(list, Comparator.comparing(AppInfo::getApp)); + return Result.ofSuccess(list); + } + + @ResponseBody + @RequestMapping(value = "/{app}/machines.json") + Result> getMachinesByApp(@PathVariable("app") String app) { + AppInfo appInfo = appManagement.getDetailApp(app); + if (appInfo == null) { + return Result.ofSuccess(null); + } + List list = new ArrayList<>(appInfo.getMachines()); + Collections.sort(list, (o1, o2) -> { + int t = o1.getApp().compareTo(o2.getApp()); + if (t != 0) { + return t; + } + t = o1.getIp().compareTo(o2.getIp()); + if (t != 0) { + return t; + } + return o1.getPort().compareTo(o2.getPort()); + }); + return Result.ofSuccess(MachineInfoVo.fromMachineInfoList(list)); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DegradeController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DegradeController.java new file mode 100755 index 00000000..2b526702 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DegradeController.java @@ -0,0 +1,200 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.DegradeRuleEntity; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; +import com.taobao.csp.sentinel.dashboard.inmem.HttpHelper; +import com.taobao.csp.sentinel.dashboard.inmem.InMemDegradeRuleStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author leyou + */ +@Controller +@RequestMapping(value = "/degrade", produces = MediaType.APPLICATION_JSON_VALUE) +public class DegradeController { + private static Logger logger = LoggerFactory.getLogger(DegradeController.class); + @Autowired + InMemDegradeRuleStore repository; + @Autowired + private HttpHelper httpHelper; + + @ResponseBody + @RequestMapping("/rules.json") + Result> queryMachineRules(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + try { + List rules = httpHelper.fetchDegradeRuleOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("queryApps error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @ResponseBody + @RequestMapping("/new.json") + Result add(String app, String ip, Integer port, String limitApp, String resource, + Double count, Integer timeWindow, Integer grade) { + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (StringUtil.isBlank(limitApp)) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (timeWindow == null) { + return Result.ofFail(-1, "timeWindow can't be null"); + } + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (grade != 0 && grade != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got"); + } + DegradeRuleEntity entity = new DegradeRuleEntity(); + entity.setApp(app.trim()); + entity.setIp(ip.trim()); + entity.setPort(port); + entity.setLimitApp(limitApp.trim()); + entity.setResource(resource.trim()); + entity.setCount(count); + entity.setTimeWindow(timeWindow); + entity.setGrade(grade); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(app, ip, port)) { + logger.info("publish degrade rules fail after rule add"); + } + return Result.ofSuccess(entity); + } + + @ResponseBody + @RequestMapping("/save.json") + Result updateIfNotNull(Long id, String app, String limitApp, String resource, + Double count, Integer timeWindow, Integer grade) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + if (grade != null) { + if (grade != 0 && grade != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got"); + } + } + DegradeRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "id " + id + " dose not exist"); + } + if (StringUtil.isNotBlank(app)) { + entity.setApp(app.trim()); + } + + if (StringUtil.isNotBlank(limitApp)) { + entity.setLimitApp(limitApp.trim()); + } + if (StringUtil.isNotBlank(resource)) { + entity.setResource(resource.trim()); + } + if (count != null) { + entity.setCount(count); + } + if (timeWindow != null) { + entity.setTimeWindow(timeWindow); + } + if (grade != null) { + entity.setGrade(grade); + } + Date date = new Date(); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("save error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.info("publish degrade rules fail after rule update"); + } + return Result.ofSuccess(entity); + } + + @ResponseBody + @RequestMapping("/delete.json") + Result delete(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.info("publish degrade rules fail after rule delete"); + } + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return httpHelper.setDegradeRuleOfMachine(app, ip, port, rules); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DemoController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DemoController.java new file mode 100755 index 00000000..8b7ab528 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/DemoController.java @@ -0,0 +1,135 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +@Controller +@RequestMapping(value = "/demo", produces = MediaType.APPLICATION_JSON_VALUE) +public class DemoController { + + Logger logger = LoggerFactory.getLogger(MachineRegistryController.class); + + @RequestMapping("/greeting") + public String greeting() { + return "index"; + } + + @RequestMapping("/link") + @ResponseBody + public String link() throws BlockException { + + Entry entry = SphU.entry("head1", EntryType.IN); + + Entry entry1 = SphU.entry("head2", EntryType.IN); + Entry entry2 = SphU.entry("head3", EntryType.IN); + Entry entry3 = SphU.entry("head4", EntryType.IN); + + entry3.exit(); + entry2.exit(); + entry1.exit(); + entry.exit(); + return "successfully create a call link"; + } + + @RequestMapping("/loop") + @ResponseBody + public String loop(String name, int time) throws BlockException { + for (int i = 0; i < 10; i++) { + Thread timer = new Thread(new RunTask(name, time, false)); + timer.setName("false"); + timer.start(); + } + return "successfully create a loop thread"; + } + + @RequestMapping("/slow") + @ResponseBody + public String slow(String name, int time) throws BlockException { + for (int i = 0; i < 10; i++) { + Thread timer = new Thread(new RunTask(name, time, true)); + timer.setName("false"); + timer.start(); + } + return "successfully create a loop thread"; + } + + static class RunTask implements Runnable { + int time; + boolean stop = false; + String name; + boolean slow = false; + + public RunTask(String name, int time, boolean slow) { + super(); + this.time = time; + this.name = name; + this.slow = slow; + } + + @Override + public void run() { + long startTime = System.currentTimeMillis(); + ContextUtil.enter(String.valueOf(startTime)); + while (!stop) { + + long now = System.currentTimeMillis(); + if (now - startTime > time * 1000) { + stop = true; + } + Entry e1 = null; + try { + e1 = SphU.entry(name); + + if (slow == true) { + TimeUnit.MILLISECONDS.sleep(3000); + } + + } catch (Exception e) { + } finally { + if (e1 != null) { + e1.exit(); + } + } + Random random2 = new Random(); + try { + TimeUnit.MILLISECONDS.sleep(random2.nextInt(200)); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + ContextUtil.exit(); + } + + } + +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/FlowController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/FlowController.java new file mode 100755 index 00000000..6aeaf305 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/FlowController.java @@ -0,0 +1,249 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.FlowRuleEntity; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; +import com.taobao.csp.sentinel.dashboard.inmem.HttpHelper; +import com.taobao.csp.sentinel.dashboard.inmem.InMemFlowRuleStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Flow rule controller. + * + * @author leyou + */ +@Controller +@RequestMapping(value = "/flow", produces = MediaType.APPLICATION_JSON_VALUE) +public class FlowController { + private static Logger logger = LoggerFactory.getLogger(FlowController.class); + @Autowired + private InMemFlowRuleStore repository; + + @Autowired + private HttpHelper httpHelper; + + @ResponseBody + @RequestMapping("/rules.json") + Result> queryMachineRules(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + try { + List rules = httpHelper.fetchFlowRuleOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("queryApps error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @ResponseBody + @RequestMapping("/new.json") + Result add(String app, String ip, Integer port, String limitApp, String resource, Integer grade, + Double count, Integer strategy, String refResource, + Integer controlBehavior, Integer warmUpPeriodSec, Integer maxQueueingTimeMs) { + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (StringUtil.isBlank(limitApp)) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (grade != 0 && grade != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got"); + } + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (strategy == null) { + return Result.ofFail(-1, "strategy can't be null"); + } + if (strategy != 0 && StringUtil.isBlank(refResource)) { + return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0"); + } + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (controlBehavior == 1 && warmUpPeriodSec == null) { + return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1"); + } + if (controlBehavior == 2 && maxQueueingTimeMs == null) { + return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2"); + } + FlowRuleEntity entity = new FlowRuleEntity(); + entity.setApp(app.trim()); + entity.setIp(ip.trim()); + entity.setPort(port); + entity.setLimitApp(limitApp.trim()); + entity.setResource(resource.trim()); + entity.setGrade(grade); + entity.setCount(count); + entity.setStrategy(strategy); + entity.setControlBehavior(controlBehavior); + entity.setWarmUpPeriodSec(warmUpPeriodSec); + entity.setMaxQueueingTimeMs(maxQueueingTimeMs); + if (strategy != 0) { + entity.setRefResource(refResource.trim()); + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(app, ip, port)) { + logger.info("publish flow rules fail after rule add"); + } + return Result.ofSuccess(entity); + } + + @ResponseBody + @RequestMapping("/save.json") + Result updateIfNotNull(Long id, String app, + String limitApp, String resource, Integer grade, + Double count, Integer strategy, String refResource, + Integer controlBehavior, Integer warmUpPeriodSec, Integer maxQueueingTimeMs) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + FlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "id " + id + " dose not exist"); + } + if (StringUtil.isNotBlank(app)) { + entity.setApp(app.trim()); + } + if (StringUtil.isNotBlank(limitApp)) { + entity.setLimitApp(limitApp.trim()); + } + if (StringUtil.isNotBlank(resource)) { + entity.setResource(resource.trim()); + } + if (grade != null) { + if (grade != 0 && grade != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got"); + } + entity.setGrade(grade); + } + if (count != null) { + entity.setCount(count); + } + if (strategy != null) { + if (strategy != 0 && strategy != 1 && strategy != 2) { + return Result.ofFail(-1, "strategy must be in [0, 1, 2], but " + strategy + " got"); + } + entity.setStrategy(strategy); + if (strategy != 0) { + if (StringUtil.isBlank(refResource)) { + return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0"); + } + entity.setRefResource(refResource.trim()); + } + } + if (controlBehavior != null) { + if (controlBehavior != 0 && controlBehavior != 1 && controlBehavior != 2) { + return Result.ofFail(-1, "controlBehavior must be in [0, 1, 2], but " + controlBehavior + " got"); + } + if (controlBehavior == 1 && warmUpPeriodSec == null) { + return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1"); + } + if (controlBehavior == 2 && maxQueueingTimeMs == null) { + return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2"); + } + entity.setControlBehavior(controlBehavior); + if (warmUpPeriodSec != null) { + entity.setWarmUpPeriodSec(warmUpPeriodSec); + } + if (maxQueueingTimeMs != null) { + entity.setMaxQueueingTimeMs(maxQueueingTimeMs); + } + } + Date date = new Date(); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + if (entity == null) { + return Result.ofFail(-1, "save entity fail"); + } + } catch (Throwable throwable) { + logger.error("save error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.info("publish flow rules fail after rule update"); + } + return Result.ofSuccess(entity); + } + + @ResponseBody + @RequestMapping("/delete.json") + Result delete(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + FlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + } catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.info("publish flow rules fail after rule delete"); + } + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return httpHelper.setFlowRuleOfMachine(app, ip, port, rules); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/HealthCheck.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/HealthCheck.java new file mode 100755 index 00000000..2afb48c7 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/HealthCheck.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class HealthCheck { + /** + * 健康检查 + */ + @GetMapping("/health") + public + @ResponseBody + String checkPreload() { + return "success"; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MachineRegistryController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MachineRegistryController.java new file mode 100755 index 00000000..f37ff928 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MachineRegistryController.java @@ -0,0 +1,71 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.Date; + +import com.taobao.csp.sentinel.dashboard.discovery.AppManagement; +import com.taobao.csp.sentinel.dashboard.discovery.MachineDiscovery; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping(value = "/registry", produces = MediaType.APPLICATION_JSON_VALUE) +public class MachineRegistryController { + Logger logger = LoggerFactory.getLogger(MachineRegistryController.class); + @Autowired + private AppManagement appManagement; + + @ResponseBody + @RequestMapping("/machine") + public Result receiveHeartBeat(String app, Long version, String hostname, String ip, Integer port) { + if (app == null) { + app = MachineDiscovery.UNKNOWN_APP_NAME; + } + if (ip == null) { + return Result.ofFail(-1, "ip can't be null"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (port == -1) { + logger.info("receive heartbeat from " + ip + " but port not set yet"); + return Result.ofFail(-1, "your port not set yet"); + } + if (version == null) { + version = System.currentTimeMillis(); + } + try { + MachineInfo machineInfo = new MachineInfo(); + machineInfo.setApp(app); + machineInfo.setHostname(hostname); + machineInfo.setIp(ip); + machineInfo.setPort(port); + machineInfo.setVersion(new Date(version)); + appManagement.addMachine(machineInfo); + return Result.ofSuccessMsg("success"); + } catch (Exception e) { + logger.error("receive heartbeat error:", e); + return Result.ofFail(-1, e.getMessage()); + } + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MetricController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MetricController.java new file mode 100755 index 00000000..11039641 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/MetricController.java @@ -0,0 +1,173 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.MetricEntity; +import com.taobao.csp.sentinel.dashboard.inmem.InMemMetricStore; +import com.taobao.csp.sentinel.dashboard.view.vo.MetricVo; + +/** + * @author leyou + */ +@Controller +@RequestMapping(value = "/metric", produces = MediaType.APPLICATION_JSON_VALUE) +public class MetricController { + + private static Logger logger = LoggerFactory.getLogger(MetricController.class); + + private static final long maxQueryIntervalMs = 1000 * 60 * 60; + + @Autowired + private InMemMetricStore metricStore; + + @ResponseBody + @RequestMapping("/queryTopResourceMetric.json") + public Result queryTopResourceMetric(final String app, + Integer pageIndex, + Integer pageSize, + Boolean desc, + Long startTime, Long endTime, String searchKey) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (pageIndex == null || pageIndex <= 0) { + pageIndex = 1; + } + if (pageSize == null) { + pageSize = 6; + } + if (pageSize >= 20) { + pageSize = 20; + } + if (desc == null) { + desc = true; + } + if (endTime == null) { + endTime = System.currentTimeMillis(); + } + if (startTime == null) { + startTime = endTime - 1000 * 60 * 5; + } + if (endTime - startTime > maxQueryIntervalMs) { + return Result.ofFail(-1, "time intervalMs is too big, must <= 1h"); + } + List resources = metricStore.findResourcesOfApp(app); + logger.info("queryTopResourceMetric(), resources.size()={}", resources.size()); + if (resources == null || resources.isEmpty()) { + return Result.ofSuccess(null); + } + if (!desc) { + Collections.reverse(resources); + } + if (StringUtil.isNotEmpty(searchKey)) { + List searched = new ArrayList<>(); + for (String resource : resources) { + if (resource.contains(searchKey)) { + searched.add(resource); + } + } + resources = searched; + } + int totalPage = (resources.size() + pageSize - 1) / pageSize; + List topResource = new ArrayList<>(); + if (pageIndex <= totalPage) { + topResource = resources.subList((pageIndex - 1) * pageSize, + Math.min(pageIndex * pageSize, resources.size())); + } + final Map> map = new ConcurrentHashMap<>(); + logger.info("topResource={}", topResource); + long time = System.currentTimeMillis(); + for (final String resource : topResource) { + List entities = metricStore.queryByAppAndResouce( + app, resource, startTime, endTime); + logger.info("resource={}, entities.size()={}", resource, entities == null ? "null" : entities.size()); + List vos = MetricVo.fromMetricEntities(entities, resource); + Iterable vosSorted = sortMetricVoAndDistinct(vos); + map.put(resource, vosSorted); + } + logger.info("queryTopResourceMetric() total query time={} ms", System.currentTimeMillis() - time); + Map resultMap = new HashMap<>(16); + resultMap.put("totalCount", resources.size()); + resultMap.put("totalPage", totalPage); + resultMap.put("pageIndex", pageIndex); + resultMap.put("pageSize", pageSize); + + Map> map2 = new LinkedHashMap<>(); + // order matters. + for (String identity : topResource) { + map2.put(identity, map.get(identity)); + } + resultMap.put("metric", map2); + return Result.ofSuccess(resultMap); + } + + @ResponseBody + @RequestMapping("/queryByAppAndResource.json") + public Result queryByAppAndResource(String app, String identity, Long startTime, Long endTime) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(identity)) { + return Result.ofFail(-1, "identity can't be null or empty"); + } + if (endTime == null) { + endTime = System.currentTimeMillis(); + } + if (startTime == null) { + startTime = endTime - 1000 * 60; + } + if (endTime - startTime > maxQueryIntervalMs) { + return Result.ofFail(-1, "time intervalMs is too big, must <= 1h"); + } + List entities = metricStore.queryByAppAndResouce( + app, identity, startTime, endTime); + List vos = MetricVo.fromMetricEntities(entities, identity); + return Result.ofSuccess(sortMetricVoAndDistinct(vos)); + } + + private Iterable sortMetricVoAndDistinct(List vos) { + if (vos == null) { + return null; + } + Map map = new TreeMap<>(); + for (MetricVo vo : vos) { + MetricVo oldVo = map.get(vo.getTimestamp()); + if (oldVo == null || vo.getGmtCreate() > oldVo.getGmtCreate()) { + map.put(vo.getTimestamp(), vo); + } + } + return map.values(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/ResourceController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/ResourceController.java new file mode 100755 index 00000000..44932775 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/ResourceController.java @@ -0,0 +1,88 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.List; +import java.util.stream.Collectors; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.command.vo.NodeVo; + +import com.taobao.csp.sentinel.dashboard.domain.ResourceTreeNode; +import com.taobao.csp.sentinel.dashboard.inmem.HttpHelper; +import com.taobao.csp.sentinel.dashboard.view.vo.ResourceVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author leyou + */ +@Controller +@RequestMapping(value = "/resource", produces = MediaType.APPLICATION_JSON_VALUE) +public class ResourceController { + + private static Logger logger = LoggerFactory.getLogger(ResourceController.class); + @Autowired + HttpHelper httpFetcher; + + /** + * Fetch real time statistics info of the machine. + * + * @param ip ip to fetch + * @param port port of the ip + * @param type one of [root, default, cluster], 'root' means fetching from tree root node, 'default' means + * fetching from tree default node, 'cluster' means fetching from cluster node. + * @param searchKey key to search + * @return node statistics info. + */ + @ResponseBody + @RequestMapping("/machineResource.json") + Result fetchIdentityOfMachine(String ip, Integer port, String type, String searchKey) { + if (StringUtil.isEmpty(ip) || port == null) { + return Result.ofFail(-1, "invalid param, give ip, port"); + } + final String ROOT = "root"; + final String DEFAULT = "default"; + if (StringUtil.isEmpty(type)) { + type = ROOT; + } + if (ROOT.equalsIgnoreCase(type) || DEFAULT.equalsIgnoreCase(type)) { + List nodeVos = httpFetcher.fetchResourceOfMachine(ip, port, type); + if (nodeVos == null) { + return Result.ofSuccess(null); + } + ResourceTreeNode treeNode = ResourceTreeNode.fromNodeVoList(nodeVos); + treeNode.searchIgnoreCase(searchKey); + return Result.ofSuccess(ResourceVo.fromResourceTreeNode(treeNode)); + } else {// cluster + List nodeVos = httpFetcher.fetchClusterNodeOfMachine(ip, port, true); + if (nodeVos == null) { + return Result.ofSuccess(null); + } + if (StringUtil.isNotEmpty(searchKey)) { + nodeVos = nodeVos.stream().filter(node -> node.getResource() + .toLowerCase().contains(searchKey.toLowerCase())) + .collect(Collectors.toList()); + } + return Result.ofSuccess(ResourceVo.fromNodeVoList(nodeVos)); + } + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/Result.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/Result.java new file mode 100755 index 00000000..1010a7b3 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/Result.java @@ -0,0 +1,76 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +/** + * @author leyou + */ +public class Result { + private int code; + private String msg; + private R data; + + public static Result ofSuccess(R data) { + Result result = new Result<>(); + result.setMsg("success"); + result.setData(data); + return result; + } + + public static Result ofSuccessMsg(String msg) { + Result result = new Result<>(); + result.setMsg(msg); + return result; + } + + public static Result ofFail(int code, String msg) { + Result result = new Result<>(); + result.setCode(code); + result.setMsg(msg); + return result; + } + + public static Result ofThrowable(int code, Throwable throwable) { + Result result = new Result<>(); + result.setCode(code); + result.setMsg(throwable.getClass().getName() + ", " + throwable.getMessage()); + return result; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public R getData() { + return data; + } + + public void setData(R data) { + this.data = data; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/SystemController.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/SystemController.java new file mode 100755 index 00000000..d195fe00 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/SystemController.java @@ -0,0 +1,214 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view; + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.SystemRuleEntity; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; +import com.taobao.csp.sentinel.dashboard.inmem.HttpHelper; +import com.taobao.csp.sentinel.dashboard.inmem.InMemSystemRuleStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author leyou(lihao) + */ +@Controller +@RequestMapping(value = "/system", produces = MediaType.APPLICATION_JSON_VALUE) +public class SystemController { + private static Logger logger = LoggerFactory.getLogger(SystemController.class); + + @Autowired + private InMemSystemRuleStore repository; + @Autowired + private HttpHelper httpHelper; + + @ResponseBody + @RequestMapping("/rules.json") + Result> queryMachineRules(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + try { + List rules = httpHelper.fetchSystemRuleOfMachine(app, ip, port); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("queryApps error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private int countNotNullAndNotNegtive(Number... values) { + int notNullCount = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] != null && values[i].doubleValue() >= 0) { + notNullCount++; + } + } + return notNullCount; + } + + @ResponseBody + @RequestMapping("/new.json") + Result add(String app, String ip, Integer port, Double avgLoad, Long avgRt, Long maxThread, Double qps) { + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + int notNullCount = countNotNullAndNotNegtive(avgLoad, avgRt, maxThread, qps); + if (notNullCount != 1) { + return Result.ofFail(-1, "only one of [avgLoad, avgRt, maxThread, qps] " + + "value must be set >= 0, but " + notNullCount + " values get"); + } + SystemRuleEntity entity = new SystemRuleEntity(); + entity.setApp(app.trim()); + entity.setIp(ip.trim()); + entity.setPort(port); + // -1 is a fake value + if (avgLoad != null) { + entity.setAvgLoad(avgLoad); + } else { + entity.setAvgLoad(-1D); + } + if (avgRt != null) { + entity.setAvgRt(avgRt); + } else { + entity.setAvgRt(-1L); + } + if (maxThread != null) { + entity.setMaxThread(maxThread); + } else { + entity.setMaxThread(-1L); + } + if (qps != null) { + entity.setQps(qps); + } else { + entity.setQps(-1D); + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(app, ip, port)) { + logger.info("publish system rules fail after rule add"); + } + return Result.ofSuccess(entity); + } + + @ResponseBody + @RequestMapping("/save.json") + Result updateIfNotNull(Long id, String app, Double avgLoad, Long avgRt, Long maxThread, Double qps) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + SystemRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "id " + id + " dose not exist"); + } + if (StringUtil.isNotBlank(app)) { + entity.setApp(app.trim()); + } + if (avgLoad != null) { + if (avgLoad < 0) { + return Result.ofFail(-1, "avgLoad must >= 0"); + } + entity.setAvgLoad(avgLoad); + } + if (avgRt != null) { + if (avgRt < 0) { + return Result.ofFail(-1, "avgRt must >= 0"); + } + entity.setAvgRt(avgRt); + } + if (maxThread != null) { + if (maxThread < 0) { + return Result.ofFail(-1, "maxThread must >= 0"); + } + entity.setMaxThread(maxThread); + } + if (qps != null) { + if (qps < 0) { + return Result.ofFail(-1, "qps must >= 0"); + } + entity.setQps(qps); + } + Date date = new Date(); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("save error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { + logger.info("publish system rules fail after rule update"); + } + return Result.ofSuccess(entity); + } + + @ResponseBody + @RequestMapping("/delete.json") + Result delete(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + SystemRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete error:", throwable); + return Result.ofThrowable(-1, throwable); + } + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.info("publish system rules fail after rule delete"); + } + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return httpHelper.setSystemRuleOfMachine(app, ip, port, rules); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MachineInfoVo.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MachineInfoVo.java new file mode 100755 index 00000000..60145f11 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MachineInfoVo.java @@ -0,0 +1,104 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view.vo; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.taobao.csp.sentinel.dashboard.discovery.MachineDiscovery; +import com.taobao.csp.sentinel.dashboard.discovery.MachineInfo; + +/** + * @author leyou + */ +public class MachineInfoVo { + private String app; + private String hostname; + private String ip; + private Integer port; + private Date version; + private boolean health; + + public static List fromMachineInfoList(List machines) { + List list = new ArrayList<>(); + for (MachineInfo machine : machines) { + list.add(fromMachineInfo(machine)); + } + return list; + } + + public static MachineInfoVo fromMachineInfo(MachineInfo machine) { + MachineInfoVo vo = new MachineInfoVo(); + vo.setApp(machine.getApp()); + vo.setHostname(machine.getHostname()); + vo.setIp(machine.getIp()); + vo.setPort(machine.getPort()); + vo.setVersion(machine.getVersion()); + if (System.currentTimeMillis() - machine.getVersion().getTime() < MachineDiscovery.MAX_CLIENT_LIVE_TIME_MS) { + vo.setHealth(true); + } + return vo; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Date getVersion() { + return version; + } + + public void setVersion(Date version) { + this.version = version; + } + + public boolean isHealth() { + return health; + } + + public void setHealth(boolean health) { + this.health = health; + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MetricVo.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MetricVo.java new file mode 100755 index 00000000..fd9296b4 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/MetricVo.java @@ -0,0 +1,208 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view.vo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.taobao.csp.sentinel.dashboard.datasource.entity.MetricEntity; + +/** + * @author leyou + */ +public class MetricVo implements Comparable { + private Long id; + private String app; + private Long timestamp; + private Long gmtCreate = System.currentTimeMillis(); + private String identity; + private Long passedQps; + private Long blockedQps; + private Long successQps; + private Long exception; + /** + * average rt + */ + private Double rt; + private Integer count; + + public MetricVo() { + } + + public static List fromMetricEntities(Collection entities) { + List list = new ArrayList<>(); + if (entities != null) { + for (MetricEntity entity : entities) { + list.add(fromMetricEntity(entity)); + } + } + return list; + } + + /** + * 保留资源名为identity的结果。 + * + * @param entities 通过hashCode查找到的MetricEntities + * @param identity 真正需要查找的资源名 + * @return + */ + public static List fromMetricEntities(Collection entities, String identity) { + List list = new ArrayList<>(); + if (entities != null) { + for (MetricEntity entity : entities) { + if (entity.getResource().equals(identity)) { + list.add(fromMetricEntity(entity)); + } + } + } + return list; + } + + public static MetricVo fromMetricEntity(MetricEntity entity) { + MetricVo vo = new MetricVo(); + vo.id = entity.getId(); + vo.app = entity.getApp(); + vo.timestamp = entity.getTimestamp().getTime(); + vo.gmtCreate = entity.getGmtCreate().getTime(); + vo.identity = entity.getResource(); + vo.passedQps = entity.getPassedQps(); + vo.blockedQps = entity.getBlockedQps(); + vo.successQps = entity.getSuccessQps(); + vo.exception = entity.getException(); + if (entity.getSuccessQps() != 0) { + vo.rt = entity.getRt() / entity.getSuccessQps(); + } else { + vo.rt = 0D; + } + vo.count = entity.getCount(); + return vo; + } + + public static MetricVo parse(String line) { + String[] strs = line.split("\\|"); + long timestamp = Long.parseLong(strs[0]); + String identity = strs[1]; + long passedQps = Long.parseLong(strs[2]); + long blockedQps = Long.parseLong(strs[3]); + long exception = Long.parseLong(strs[4]); + double rt = Double.parseDouble(strs[5]); + long successQps = Long.parseLong(strs[6]); + MetricVo vo = new MetricVo(); + vo.timestamp = timestamp; + vo.identity = identity; + vo.passedQps = passedQps; + vo.blockedQps = blockedQps; + vo.successQps = successQps; + vo.exception = exception; + vo.rt = rt; + vo.count = 1; + return vo; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public Long getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Long gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + + public Long getPassedQps() { + return passedQps; + } + + public void setPassedQps(Long passedQps) { + this.passedQps = passedQps; + } + + public Long getBlockedQps() { + return blockedQps; + } + + public void setBlockedQps(Long blockedQps) { + this.blockedQps = blockedQps; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + public Long getException() { + return exception; + } + + public void setException(Long exception) { + this.exception = exception; + } + + public Double getRt() { + return rt; + } + + public void setRt(Double rt) { + this.rt = rt; + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + @Override + public int compareTo(MetricVo o) { + return Long.compare(this.timestamp, o.timestamp); + } +} diff --git a/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/ResourceVo.java b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/ResourceVo.java new file mode 100755 index 00000000..a84096a3 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/taobao/csp/sentinel/dashboard/view/vo/ResourceVo.java @@ -0,0 +1,241 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.taobao.csp.sentinel.dashboard.view.vo; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.command.vo.NodeVo; + +import com.taobao.csp.sentinel.dashboard.domain.ResourceTreeNode; + +/** + * @author leyou + */ +public class ResourceVo { + private String parentTtId; + private String ttId; + private String resource; + + private Integer threadNum; + private Long passQps; + private Long blockedQps; + private Long totalQps; + private Long averageRt; + private Long passRequestQps; + private Long exceptionQps; + private Long oneMinutePassed; + private Long oneMinuteBlocked; + private Long oneMinuteException; + private Long oneMinuteTotal; + + private boolean visible = true; + + public ResourceVo() { + } + + public static List fromNodeVoList(List nodeVos) { + if (nodeVos == null) { + return null; + } + List list = new ArrayList<>(); + boolean isFirst = true; + for (NodeVo nodeVo : nodeVos) { + if (isFirst) { + isFirst = false; + continue; + } + ResourceVo vo = new ResourceVo(); + vo.parentTtId = nodeVo.getParentId(); + vo.ttId = nodeVo.getId(); + vo.resource = nodeVo.getResource(); + vo.threadNum = nodeVo.getThreadNum(); + vo.passQps = nodeVo.getPassQps(); + vo.blockedQps = nodeVo.getBlockedQps(); + vo.totalQps = nodeVo.getTotalQps(); + vo.averageRt = nodeVo.getAverageRt(); + vo.exceptionQps = nodeVo.getExceptionQps(); + vo.oneMinutePassed = nodeVo.getOneMinutePassed(); + vo.oneMinuteBlocked = nodeVo.getOneMinuteBlocked(); + vo.oneMinuteException = nodeVo.getOneMinuteException(); + vo.oneMinuteTotal = nodeVo.getOneMinuteTotal(); + list.add(vo); + } + return list; + } + + public static List fromResourceTreeNode(ResourceTreeNode root) { + if (root == null) { + return null; + } + List list = new ArrayList<>(); + visit(root, list, false, true); + //if(!list.isEmpty()){ + // list.remove(0); + //} + return list; + } + + /** + * This node is visible when this.visible==true or one of this's parents is visible, + * root node is always invisible. + */ + private static void visit(ResourceTreeNode node, List list, boolean parentVisible, boolean isRoot) { + boolean visible = !isRoot && (node.isVisible() || parentVisible); + //boolean visible = node.isVisible(); + if (visible) { + ResourceVo vo = new ResourceVo(); + vo.parentTtId = node.getParentId(); + vo.ttId = node.getId(); + vo.resource = node.getResource(); + vo.threadNum = node.getThreadNum(); + vo.passQps = node.getPassQps(); + vo.blockedQps = node.getBlockedQps(); + vo.totalQps = node.getTotalQps(); + vo.averageRt = node.getAverageRt(); + vo.exceptionQps = node.getExceptionQps(); + vo.oneMinutePassed = node.getOneMinutePassed(); + vo.oneMinuteBlocked = node.getOneMinuteBlocked(); + vo.oneMinuteException = node.getOneMinuteException(); + vo.oneMinuteTotal = node.getOneMinuteTotal(); + vo.visible = node.isVisible(); + list.add(vo); + } + for (ResourceTreeNode c : node.getChildren()) { + visit(c, list, visible, false); + } + } + + public String getParentTtId() { + return parentTtId; + } + + public void setParentTtId(String parentTtId) { + this.parentTtId = parentTtId; + } + + public String getTtId() { + return ttId; + } + + public void setTtId(String ttId) { + this.ttId = ttId; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getThreadNum() { + return threadNum; + } + + public void setThreadNum(Integer threadNum) { + this.threadNum = threadNum; + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockedQps() { + return blockedQps; + } + + public void setBlockedQps(Long blockedQps) { + this.blockedQps = blockedQps; + } + + public Long getTotalQps() { + return totalQps; + } + + public void setTotalQps(Long totalQps) { + this.totalQps = totalQps; + } + + public Long getAverageRt() { + return averageRt; + } + + public void setAverageRt(Long averageRt) { + this.averageRt = averageRt; + } + + public Long getPassRequestQps() { + return passRequestQps; + } + + public void setPassRequestQps(Long passRequestQps) { + this.passRequestQps = passRequestQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public Long getOneMinuteException() { + return oneMinuteException; + } + + public void setOneMinuteException(Long oneMinuteException) { + this.oneMinuteException = oneMinuteException; + } + + public Long getOneMinutePassed() { + return oneMinutePassed; + } + + public void setOneMinutePassed(Long oneMinutePassed) { + this.oneMinutePassed = oneMinutePassed; + } + + public Long getOneMinuteBlocked() { + return oneMinuteBlocked; + } + + public void setOneMinuteBlocked(Long oneMinuteBlocked) { + this.oneMinuteBlocked = oneMinuteBlocked; + } + + public Long getOneMinuteTotal() { + return oneMinuteTotal; + } + + public void setOneMinuteTotal(Long oneMinuteTotal) { + this.oneMinuteTotal = oneMinuteTotal; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } +} diff --git a/sentinel-dashboard/src/main/resources/application.properties b/sentinel-dashboard/src/main/resources/application.properties new file mode 100755 index 00000000..3dcc24cb --- /dev/null +++ b/sentinel-dashboard/src/main/resources/application.properties @@ -0,0 +1,11 @@ +#spring settings +spring.http.encoding.force=true +spring.http.encoding.charset=UTF-8 +spring.http.encoding.enabled=true + +#logging settings +logging.level.org.springframework.web=INFO +logging.file=${user.home}/logs/csp/sentinel-dashboard.log +logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n +logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + diff --git a/sentinel-dashboard/src/main/webapp/resources/.gitignore b/sentinel-dashboard/src/main/webapp/resources/.gitignore new file mode 100755 index 00000000..104d9996 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +tmp/ \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/.jshintrc b/sentinel-dashboard/src/main/webapp/resources/.jshintrc new file mode 100755 index 00000000..6c66001a --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/.jshintrc @@ -0,0 +1,67 @@ +{ + /* + * ENVIRONMENTS + * ================= + */ + + // Define globals exposed by modern browsers. + "browser": true, + + // Define globals exposed by jQuery. + "jquery": true, + + // Define globals exposed by Node.js. + "node": true, + + // Allow ES6. + "esversion": 6, + + /* + * ENFORCING OPTIONS + * ================= + */ + + // Force all variable names to use either camelCase style or UPPER_CASE + // with underscores. + "camelcase": true, + + // Prohibit use of == and != in favor of === and !==. + "eqeqeq": true, + + // Enforce tab width of 2 spaces. + "indent": 2, + + // Prohibit use of a variable before it is defined. + "latedef": true, + + // Enforce line length to 100 characters + "maxlen": 100, + + // Require capitalized names for constructor functions. + "newcap": true, + + // Enforce use of single quotation marks for strings. + "quotmark": "single", + + // Enforce placing 'use strict' at the top function scope + // 前端项目中外层使用 strict 即可,覆盖此条规则 + "strict": false, + + // Prohibit use of explicitly undeclared variables. + "undef": true, + + // Warn when variables are defined but never used. + "unused": true, + + /* + * RELAXING OPTIONS + * ================= + */ + + // Suppress warnings about == null comparisons. + "eqnull": true, + "globals": { + "$": false, + "angular": false + } +} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/README.md b/sentinel-dashboard/src/main/webapp/resources/README.md new file mode 100755 index 00000000..b186fd8a --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/README.md @@ -0,0 +1,32 @@ +# Sentinel-Dashboard + +## Env Requirement + +- Node.js > 6.x + +## Code Guide + +- Code Style Guide for HTML/CSS: https://codeguide.bootcss.com/ +- Airbnb JavaScript Style Guide: https://github.com/airbnb/javascript/tree/es5-deprecated/es5 + +## Install Packages + +``` +npm install +``` + +## Start Development + +``` +npm start +``` + +## Build for production + +``` +npm run build +``` + +## CREDIT: + +- [sb-admin-angular](https://github.com/start-angular/sb-admin-angular) diff --git a/sentinel-dashboard/src/main/webapp/resources/README_zh.md b/sentinel-dashboard/src/main/webapp/resources/README_zh.md new file mode 100644 index 00000000..c7893ad3 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/README_zh.md @@ -0,0 +1,32 @@ +# Sentinel-Dashboard + +## 环境要求 + +- Node.js > 6.x + +## 编码规范 + +- HTML/CSS 遵循 Bootstrap 编码规范,参考:https://codeguide.bootcss.com/ +- JavaScript 遵循 Airbnb JavaScript Style Guide,参考:https://github.com/airbnb/javascript/tree/es5-deprecated/es5 + +## 安装依赖 + +``` +npm i +``` + +## 开始本地开发 + +``` +npm start +``` + +## 构建前端资源 + +``` +npm run build +``` + +## CREDIT: + +- [sb-admin-angular](https://github.com/start-angular/sb-admin-angular) diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js new file mode 100755 index 00000000..5d7cec96 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js @@ -0,0 +1,163 @@ +'use strict'; + +/** + * @ngdoc overview + * @name sentinelDashboardApp + * @description + * # sentinelDashboardApp + * + * Main module of the application. + */ + +angular + .module('sentinelDashboardApp', [ + 'oc.lazyLoad', + 'ui.router', + 'ui.bootstrap', + 'angular-loading-bar', + 'ngDialog', + 'ui.bootstrap.datetimepicker', + 'ui-notification', + 'rzTable', + 'angular-clipboard', + 'selectize', + 'angularUtils.directives.dirPagination' + ]) + .config(['$stateProvider', '$urlRouterProvider', '$ocLazyLoadProvider', + function ($stateProvider, $urlRouterProvider, $ocLazyLoadProvider) { + $ocLazyLoadProvider.config({ + debug: false, + events: true, + }); + + $urlRouterProvider.otherwise('/dashboard/home'); + + $stateProvider + .state('dashboard', { + url: '/dashboard', + templateUrl: 'app/views/dashboard/main.html', + resolve: { + loadMyDirectives: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load( + { + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/directives/header/header.js', + 'app/scripts/directives/sidebar/sidebar.js', + 'app/scripts/directives/sidebar/sidebar-search/sidebar-search.js', + ] + }); + }] + } + }) + + .state('dashboard.home', { + url: '/home', + templateUrl: 'app/views/dashboard/home.html', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/main.js', + ] + }); + }] + } + }) + + .state('dashboard.flow', { + templateUrl: 'app/views/flow.html', + url: '/flow/:app', + controller: 'FlowCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/flow.js', + ] + }); + }] + } + }) + + .state('dashboard.degrade', { + templateUrl: 'app/views/degrade.html', + url: '/degrade/:app', + controller: 'DegradeCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/degrade.js', + ] + }); + }] + } + }) + + .state('dashboard.system', { + templateUrl: 'app/views/system.html', + url: '/system/:app', + controller: 'SystemCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/system.js', + ] + }); + }] + } + }) + + .state('dashboard.machine', { + templateUrl: 'app/views/machine.html', + url: '/app/:app', + controller: 'MachineCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/machine.js', + ] + }); + }] + } + }) + + .state('dashboard.identity', { + templateUrl: 'app/views/identity.html', + url: '/identity/:app', + controller: 'IdentityCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/identity.js', + ] + }); + }] + } + }) + .state('dashboard.metric', { + templateUrl: 'app/views/metric.html', + url: '/metric/:app', + controller: 'MetricCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/metric.js', + ] + }); + }] + } + }); + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/degrade.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/degrade.js new file mode 100755 index 00000000..c31752bb --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/degrade.js @@ -0,0 +1,187 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('DegradeCtl', ['$scope', '$stateParams', 'DegradeService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, DegradeService, ngDialog, MachineService) { + //初始化 + $scope.app = $stateParams.app; + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + DegradeService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + var degradeRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = rule; + $scope.degradeRuleDialog = { + title: '编辑降级规则', + type: 'edit', + confirmBtnText: '保存' + }; + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + limitApp: 'default' + }; + $scope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增' + }; + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if ($scope.degradeRuleDialog.type == 'add') { + addNewRule($scope.currentRule); + } else if ($scope.degradeRuleDialog.type == 'edit') { + saveRule($scope.currentRule, true); + } + } + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除降级规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下降级规则', + attention: '资源名: ' + rule.resource + ', 降级应用: ' + rule.limitApp + + ', 阈值类型: ' + (rule.grade == 0 ? 'RT' : '异常比例') + ', 阈值: ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + DegradeService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + function addNewRule(rule) { + DegradeService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + degradeRuleDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + function saveRule(rule, edit) { + DegradeService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + degradeRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('失败!'); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.health) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow.js new file mode 100755 index 00000000..556b26b6 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/flow.js @@ -0,0 +1,200 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('FlowCtl', ['$scope', '$stateParams', 'FlowService', 'ngDialog', + 'MachineService', + function ($scope, $stateParams, FlowService, ngDialog, + MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + FlowService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + var flowRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = rule; + $scope.flowRuleDialog = { + title: '编辑流控规则', + type: 'edit', + confirmBtnText: '保存', + showAdvanceButton: rule.controlBehavior == 0 && rule.strategy == 0 + }; + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 1, + strategy: 0, + controlBehavior: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + limitApp: 'default' + }; + $scope.flowRuleDialog = { + title: '新增流控规则', + type: 'add', + confirmBtnText: '新增', + showAdvanceButton: true, + }; + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if ($scope.flowRuleDialog.type == 'add') { + addNewRule($scope.currentRule); + } else if ($scope.flowRuleDialog.type == 'edit') { + saveRule($scope.currentRule, true); + } + } + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除流控规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下流控规则', + attention: '资源名: ' + rule.resource + ', 流控应用: ' + rule.limitApp + + ', 阈值类型: ' + (rule.grade == 0 ? '线程数' : 'QPS') + ', 阈值: ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + FlowService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + function addNewRule(rule) { + FlowService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + flowRuleDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + $scope.onOpenAdvanceClick = function () { + $scope.flowRuleDialog.showAdvanceButton = false; + }; + $scope.onCloseAdvanceClick = function () { + $scope.flowRuleDialog.showAdvanceButton = true; + }; + + function saveRule(rule, edit) { + FlowService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + flowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('失败!'); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.health) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/home.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/home.js new file mode 100755 index 00000000..1df5862c --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/home.js @@ -0,0 +1,11 @@ +/** + * @ngdoc function + * @name sentinelDashboardApp.controller:MainCtrl + * @description + * # MainCtrl + * Controller of the sentinelDashboardApp + */ +angular.module('sentinelDashboardApp') + .controller('HomeCtrl', ['$scope', '$position', function ($scope, $position) { + // do noting + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js new file mode 100755 index 00000000..f464e0c2 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/identity.js @@ -0,0 +1,286 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('IdentityCtl', ['$scope', '$stateParams', 'IdentityService', + 'ngDialog', 'FlowService', 'DegradeService', 'MachineService', + '$interval', '$location', '$timeout', + function ($scope, $stateParams, IdentityService, ngDialog, + FlowService, DegradeService, MachineService, $interval, $location, $timeout) { + + $scope.app = $stateParams.app; + // $scope.rulesPageConfig = { + // pageSize : 10, + // currentPageIndex : 1, + // totalPage : 1, + // totalCount: 0, + // }; + $scope.currentPage = 1; + $scope.pageSize = 16; + $scope.totalPage = 1; + $scope.totalCount = 0; + $scope.identities = []; + // 数据自动刷新频率, 默认10s + var DATA_REFRESH_INTERVAL = 30; + + $scope.isExpand = true; + $scope.searchKey = ''; + $scope.firstExpandAll = false; + $scope.isTreeView = true; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + $scope.table = null; + + var flowRuleDialog; + var flowRuleDialogScope; + $scope.addNewFlowRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + flowRuleDialogScope = $scope.$new(true); + flowRuleDialogScope.currentRule = { + enable: false, + strategy: 0, + grade: 1, + controlBehavior: 0, + resource: resource, + limitApp: 'default', + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + flowRuleDialogScope.flowRuleDialog = { + title: '新增流控规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加', + showAdvanceButton: true + }; + // $scope.flowRuleDialog = { + // showAdvanceButton : true + // }; + flowRuleDialogScope.saveRule = saveFlowRule; + flowRuleDialogScope.saveRuleAndContinue = saveFlowRuleAndContinue; + flowRuleDialogScope.onOpenAdvanceClick = function () { + flowRuleDialogScope.flowRuleDialog.showAdvanceButton = false; + }; + flowRuleDialogScope.onCloseAdvanceClick = function () { + flowRuleDialogScope.flowRuleDialog.showAdvanceButton = true; + }; + + flowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/flow-rule-dialog.html', + width: 680, + overlay: true, + scope: flowRuleDialogScope + }); + }; + + function saveFlowRule() { + FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + flowRuleDialog.close(); + var url = '/dashboard/flow/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }); + } + + function saveFlowRuleAndContinue() { + FlowService.newRule(flowRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + flowRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var degradeRuleDialog; + var degradeRuleDialogScope; + $scope.addNewDegradeRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + degradeRuleDialogScope = $scope.$new(true); + degradeRuleDialogScope.currentRule = { + enable: false, + grade: 0, + strategy: 0, + resource: resource, + limitApp: 'default', + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + degradeRuleDialogScope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加' + }; + degradeRuleDialogScope.saveRule = saveDegradeRule; + degradeRuleDialogScope.saveRuleAndContinue = saveDegradeRuleAndContinue; + + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: degradeRuleDialogScope + }); + }; + + function saveDegradeRule() { + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + var url = '/dashboard/degrade/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }); + } + + function saveDegradeRuleAndContinue() { + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var searchHandler; + $scope.searchChange = function (searchKey) { + // console.info('searchKey=', searchKey); + $timeout.cancel(searchHandler); + searchHandler = $timeout(function () { + $scope.searchKey = searchKey; + $scope.isExpand = true; + $scope.firstExpandAll = true; + reInitIdentityDatas(); + $scope.firstExpandAll = false; + }, 600); + } + + $scope.initTreeTable = function () { + // if (!$scope.table) { + com_github_culmat_jsTreeTable.register(window); + $scope.table = window.treeTable($('#identities')); + // } + } + + $scope.expandAll = function () { + $scope.isExpand = true; + }; + $scope.collapseAll = function () { + $scope.isExpand = false; + }; + $scope.treeView = function () { + $scope.isTreeView = true; + queryIdentities(); + } + $scope.listView = function () { + $scope.isTreeView = false; + queryIdentities(); + } + + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.health) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + reInitIdentityDatas(); + } + }); + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + + var intervalId; + function reInitIdentityDatas() { + // $interval.cancel(intervalId); + queryIdentities(); + // intervalId = $interval(function () { + // queryIdentities(); + // }, DATA_REFRESH_INTERVAL * 1000); + }; + + function queryIdentities() { + var mac = $scope.macInputModel.split(':'); + if (mac == null || mac.length < 2) { + return; + } + if ($scope.isTreeView) { + IdentityService.fetchIdentityOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + } else { + IdentityService.fetchClusterNodeOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + } + }; + $scope.queryIdentities = queryIdentities; + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/machine.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/machine.js new file mode 100755 index 00000000..08c9f71f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/machine.js @@ -0,0 +1,45 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('MachineCtl', ['$scope', '$stateParams', 'MachineService', + function ($scope, $stateParams, MachineService) { + $scope.app = $stateParams.app; + $scope.propertyName = ''; + $scope.reverse = false; + $scope.currentPage = 1; + $scope.machines = []; + $scope.machinesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.sortBy = function (propertyName) { + // console.log('machine sortBy ' + propertyName); + $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false; + $scope.propertyName = propertyName; + }; + + MachineService.getAppMachines($scope.app).success( + function (data) { + // console.log('get machines: ' + data.data[0].hostname) + if (data.code == 0 && data.data) { + $scope.machines = data.data; + var health = 0; + $scope.machines.forEach(function (item) { + if (item.health) { + health++; + } + if (!item.hostname) { + item.hostname = '未知' + } + }) + $scope.healthCount = health; + $scope.machinesPageConfig.totalCount = $scope.machines.length; + } else { + $scope.machines = []; + $scope.healthCount = 0; + } + } + ); + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/main.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/main.js new file mode 100755 index 00000000..37500f7e --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/main.js @@ -0,0 +1,10 @@ +/** + * @ngdoc function + * @name sentinelDashboardApp.controller:MainCtrl + * @description + * # MainCtrl + * Controller of the sentinelDashboardApp + */ +angular.module('sentinelDashboardApp') + .controller('DashboardCtrl', ['$scope', '$position', function ($scope, $position) { + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/metric.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/metric.js new file mode 100755 index 00000000..8f853007 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/metric.js @@ -0,0 +1,251 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('MetricCtl', ['$scope', '$stateParams', 'MetricService', '$interval', '$timeout', + function ($scope, $stateParams, MetricService, $interval, $timeout) { + + $scope.endTime = new Date(); + $scope.startTime = new Date(); + $scope.startTime.setMinutes($scope.endTime.getMinutes() - 30); + $scope.startTimeFmt = formatDate($scope.startTime); + $scope.endTimeFmt = formatDate($scope.endTime); + function formatDate(date) { + return moment(date).format('YYYY/MM/DD HH:mm:ss'); + } + $scope.changeStartTime = function (startTime) { + $scope.startTime = new Date(startTime); + $scope.startTimeFmt = formatDate(startTime); + }; + $scope.changeEndTime = function (endTime) { + $scope.endTime = new Date(endTime); + $scope.endTimeFmt = formatDate(endTime); + }; + + $scope.app = $stateParams.app; + // 数据自动刷新频率 + var DATA_REFRESH_INTERVAL = 1000 * 10; + + $scope.servicePageConfig = { + pageSize: 6, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.servicesChartConfigs = []; + + $scope.pageChanged = function (newPageNumber) { + $scope.servicePageConfig.currentPageIndex = newPageNumber; + reInitIdentityDatas(); + }; + + var searchT; + $scope.searchService = function () { + $timeout.cancel(searchT); + searchT = $timeout(function () { + reInitIdentityDatas(); + }, 600); + } + + var intervalId; + reInitIdentityDatas(); + function reInitIdentityDatas() { + $interval.cancel(intervalId); + queryIdentityDatas(); + intervalId = $interval(function () { + queryIdentityDatas(); + }, DATA_REFRESH_INTERVAL); + }; + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + $scope.initAllChart = function () { + $.each($scope.metrics, function (idx, metric) { + if (idx == $scope.metrics.length - 1) { + return; + } + const chart = new G2.Chart({ + container: 'chart' + idx, + forceFit: true, + width: 100, + height: 250, + padding: [10, 30, 70, 30] + }); + chart.source(metric.data); + chart.scale('timestamp', { + type: 'time', + mask: 'YYYY-MM-DD HH:mm:ss' + }); + chart.scale('passedQps', { + min: 0, + fine: true, + alias: 'p_qps' + // max: 10 + }); + chart.scale('blockedQps', { + min: 0, + fine: true, + alias: 'b_qps', + }); + chart.scale('rt', { + min: 0, + fine: true, + }); + chart.axis('rt', { + grid: null, + label: null + }); + chart.axis('blockedQps', { + grid: null, + label: null + }); + + chart.axis('timestamp', { + label: { + textStyle: { + textAlign: 'center', // 文本对齐方向,可取值为: start center end + fill: '#404040', // 文本的颜色 + fontSize: '11', // 文本大小 + textBaseline: 'top', // 文本基准线,可取 top middle bottom,默认为middle + }, + autoRotate: false, + formatter: function (text, item, index) { + return text.substring(11, 11 + 5); + } + } + }); + chart.legend({ + custom: true, + allowAllCanceled: true, + itemFormatter: function (val) { + // console.log('val=', val); + if ('passedQps' === val) { + return 'p_qps'; + } + if ('blockedQps' === val) { + return 'b_qps'; + } + return val; + }, + items: [ + { value: 'passedQps', marker: { symbol: 'hyphen', stroke: 'green', radius: 5, lineWidth: 2 } }, + { value: 'blockedQps', marker: { symbol: 'hyphen', stroke: 'blue', radius: 5, lineWidth: 2 } }, + // { value: 'rt', marker: {symbol: 'hyphen', stroke: 'gray', radius: 5, lineWidth: 2} }, + ], + onClick: function (ev) { + const item = ev.item; + const value = item.value; + const checked = ev.checked; + const geoms = chart.getAllGeoms(); + for (var i = 0; i < geoms.length; i++) { + const geom = geoms[i]; + if (geom.getYScale().field === value) { + if (checked) { + geom.show(); + } else { + geom.hide(); + } + } + } + } + }); + chart.line().position('timestamp*passedQps').size(1).color('green').shape('smooth'); + chart.line().position('timestamp*blockedQps').size(1).color('blue').shape('smooth'); + // chart.line().position('timestamp*rt').size(1).color('gray'); + G2.track(false); + chart.render(); + }); + }; + + $scope.metrics = []; + $scope.emptyObjs = []; + function queryIdentityDatas() { + var params = { + app: $scope.app, + pageIndex: $scope.servicePageConfig.currentPageIndex, + pageSize: $scope.servicePageConfig.pageSize, + desc: $scope.isDescOrder, + searchKey: $scope.serviceQuery + }; + MetricService.queryAppSortedIdentities(params).success(function (data) { + $scope.metrics = []; + $scope.emptyObjs = []; + if (data.code == 0 && data.data) { + var metricsObj = data.data.metric; + var identityNames = Object.keys(metricsObj); + if (identityNames.length < 1) { + $scope.emptyServices = true; + } else { + $scope.emptyServices = false; + } + $scope.servicePageConfig.totalPage = data.data.totalPage; + $scope.servicePageConfig.pageSize = data.data.pageSize; + var totalCount = data.data.totalCount; + $scope.servicePageConfig.totalCount = totalCount; + for (i = 0; i < totalCount; i++) { + $scope.emptyObjs.push({}); + } + $.each(identityNames, function (idx, identityName) { + var identityDatas = metricsObj[identityName]; + var metrics = {}; + metrics.resource = identityName; + // metrics.data = identityDatas; + metrics.data = fillZeros(identityDatas); + metrics.shortData = lastOfArray(identityDatas, 6); + $scope.metrics.push(metrics); + }); + // push an empty element in the last, for ng-init reasons. + $scope.metrics.push([]); + } else { + $scope.emptyServices = true; + console.log(data.msg); + } + }); + }; + function fillZeros(metricData) { + if (!metricData || metricData.length == 0) { + return []; + } + var filledData = []; + filledData.push(metricData[0]); + var lastTime = metricData[0].timestamp / 1000; + for (var i = 1; i < metricData.length; i++) { + var curTime = metricData[i].timestamp / 1000; + if (curTime > lastTime + 1) { + for (var j = lastTime + 1; j < curTime; j++) { + filledData.push({ + "timestamp": j * 1000, + "passedQps": 0, + "blockedQps": 0, + "successQps": 0, + "exception": 0, + "rt": 0, + "count": 0 + }) + } + } + filledData.push(metricData[i]); + lastTime = curTime; + } + return filledData; + } + function lastOfArray(arr, n) { + if (!arr.length) { + return []; + } + var rs = []; + for (i = 0; i < n && i < arr.length; i++) { + rs.push(arr[arr.length - 1 - i]); + } + return rs; + } + + $scope.isDescOrder = true; + $scope.setDescOrder = function () { + $scope.isDescOrder = true; + reInitIdentityDatas(); + } + $scope.setAscOrder = function () { + $scope.isDescOrder = false; + reInitIdentityDatas(); + } + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js new file mode 100755 index 00000000..c08d418f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/system.js @@ -0,0 +1,230 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('SystemCtl', ['$scope', '$stateParams', 'SystemService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, SystemService, + ngDialog, MachineService) { + //初始化 + $scope.app = $stateParams.app; + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + SystemService.queryMachineRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $.each($scope.rules, function (idx, rule) { + // rule.orginEnable = rule.enable; + if (rule.avgLoad >= 0) { + rule.grade = 0; + } else if (rule.avgRt >= 0) { + rule.grade = 1; + } else if (rule.maxThread >= 0) { + rule.grade = 2; + } else if (rule.qps >= 0) { + rule.grade = 3; + } + }); + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + var systemRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = rule; + $scope.systemRuleDialog = { + title: '编辑系统保护规则', + type: 'edit', + confirmBtnText: '保存' + }; + systemRuleDialog = ngDialog.open({ + template: '/app/views/dialog/system-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + }; + $scope.systemRuleDialog = { + title: '新增系统保护规则', + type: 'add', + confirmBtnText: '新增' + }; + systemRuleDialog = ngDialog.open({ + template: '/app/views/dialog/system-rule-dialog.html', + width: 680, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if ($scope.systemRuleDialog.type == 'add') { + addNewRule($scope.currentRule); + } else if ($scope.systemRuleDialog.type == 'edit') { + saveRule($scope.currentRule, true); + } + } + + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + var ruleTypeDesc = ''; + var ruleTypeCount = null; + if (rule.avgLoad != -1) { + ruleTypeDesc = 'LOAD'; + ruleTypeCount = rule.avgLoad; + } else if (rule.avgRt != -1) { + ruleTypeDesc = 'RT'; + ruleTypeCount = rule.avgRt; + } else if (rule.maxThread != -1) { + ruleTypeDesc = '线程数'; + ruleTypeCount = rule.maxThread; + } else if (rule.qps != -1) { + ruleTypeDesc = 'QPS'; + ruleTypeCount = rule.qps; + } + + $scope.confirmDialog = { + title: '删除系统保护规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下系统保护规则', + attention: '阈值类型: ' + ruleTypeDesc + ', 阈值: ' + ruleTypeCount, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + // } else if ($scope.confirmDialog.type == 'enable_rule') { + // $scope.currentRule.enable = true; + // saveRule($scope.currentRule); + // } else if ($scope.confirmDialog.type == 'disable_rule') { + // $scope.currentRule.enable = false; + // saveRule($scope.currentRule); + // } else if ($scope.confirmDialog.type == 'enable_all') { + // enableAll($scope.app); + // } else if ($scope.confirmDialog.type == 'disable_all') { + // disableAll($scope.app); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + SystemService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert + } + }); + }; + + function addNewRule(rule) { + SystemService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + systemRuleDialog.close(); + } else { + alert('失败!'); + } + }); + }; + + function saveRule(rule, edit) { + SystemService.saveRule(rule).success(function (data) { + if (data.code == 0) { + // if (rule.enable) { + // rule.orginEnable = true; + // } else { + // rule.orginEnable = false; + // } + getMachineRules(); + if (edit) { + systemRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('失败!'); + } + }); + } + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.health) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + } + }); + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html new file mode 100755 index 00000000..864430eb --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.html @@ -0,0 +1,9 @@ +
+ + + +
\ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.js new file mode 100755 index 00000000..d7f625ac --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/header/header.js @@ -0,0 +1,16 @@ +/** + * @ngdoc directive + * @name izzyposWebApp.directive:adminPosHeader + * @description + * # adminPosHeader + */ +angular.module('sentinelDashboardApp') + .directive('header', [function () { + return { + templateUrl: 'app/scripts/directives/header/header.html', + restrict: 'E', + replace: true, + controller: function ($scope) { + } + } + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html new file mode 100755 index 00000000..18b2b3a9 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js new file mode 100755 index 00000000..31acca6e --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar-search/sidebar-search.js @@ -0,0 +1,20 @@ +/** + * @ngdoc directive + * @name izzyposWebApp.directive:adminPosHeader + * @description + * # adminPosHeader + */ + +angular.module('sentinelDashboardApp') + .directive('sidebarSearch', function () { + return { + templateUrl: 'app/scripts/directives/sidebar/sidebar-search/sidebar-search.html', + restrict: 'E', + replace: true, + scope: { + }, + controller: function ($scope) { + $scope.selectedMenu = 'home'; + } + } + }); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html new file mode 100755 index 00000000..4a829aa6 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html @@ -0,0 +1,72 @@ + \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js new file mode 100755 index 00000000..c81e9674 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js @@ -0,0 +1,62 @@ +/** + * @ngdoc directive + * @name izzyposWebApp.directive:adminPosHeader + * @description # adminPosHeader + */ + +angular.module('sentinelDashboardApp') + .directive('sidebar', ['$location', '$stateParams', 'AppService', function () { + return { + templateUrl: 'app/scripts/directives/sidebar/sidebar.html', + restrict: 'E', + replace: true, + scope: { + }, + controller: function ($scope, $stateParams, $location, AppService) { + $scope.app = $stateParams.app; + $scope.collapseVar = 0; + + // app + AppService.getApps().success( + function (data) { + if (data.code == 0) { + var initHashApp = $location.path().split('/')[3]; + $scope.apps = data.data; + $scope.apps.forEach(function (item) { + if (item.app == initHashApp) { + item.active = true; + } + }); + } + } + ); + + // toggle side bar + $scope.click = function ($event) { + var element = angular.element($event.target); + var entry = angular.element($event.target).scope().entry; + entry.active = !entry.active; + + if (entry.active == false) { + element.parent().children('ul').hide(); + } else { + element.parent().parent().children('li').children('ul').hide(); + element.parent().children('ul').show(); + } + } + + $scope.addSearchApp = function () { + var findApp = false; + for (var i = 0; i < $scope.apps.length; i++) { + if ($scope.apps[i].app == $scope.searchApp) { + findApp = true; + break; + } + } + if (!findApp) { + $scope.apps.push({ app: $scope.searchApp }); + } + } + } + } + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/filters/filters.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/filters/filters.js new file mode 100755 index 00000000..f39b08f7 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/filters/filters.js @@ -0,0 +1,17 @@ +var app = angular.module('sentinelDashboardApp'); + +app.filter('range', [function () { + return function (input, length) { + if (isNaN(length) || length <= 0) { + return []; + } + + input = []; + for (var index = 1; index <= length; index++) { + input.push(index); + } + + return input; + }; + +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/libs/treeTable.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/libs/treeTable.js new file mode 100755 index 00000000..cebc2561 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/libs/treeTable.js @@ -0,0 +1,292 @@ +var com_github_culmat_jsTreeTable = (function(){ + + function depthFirst(tree, func, childrenAttr) { + childrenAttr = childrenAttr || 'children' + function i_depthFirst(node) { + if (node[childrenAttr]) { + $.each(node[childrenAttr], function(i, child) { + i_depthFirst(child) + }) + } + func(node) + } + $.each(tree, function(i, root) { + i_depthFirst(root) + }) + return tree + } + + /* + * make a deep copy of the object + */ + function copy(data){ + return JSON.parse(JSON.stringify(data)) + } + + function makeTree (data, idAttr, refAttr, childrenAttr) { + var data_tmp = data + idAttr = idAttr || 'id' + refAttr = refAttr || 'parent' + childrenAttr = childrenAttr || 'children' + + var byName = [] + $.each(data_tmp, function(i, entry) { + byName[entry[idAttr]] = entry + }) + var tree = [] + $.each(data_tmp, function(i, entry) { + var parents = entry[refAttr] + if(!$.isArray(parents)){ + parents = [parents] + } + if(parents.length == 0){ + tree.push(entry) + } else { + var inTree = false; + $.each(parents, function(i,parentID){ + var parent = byName[parentID] + if (parent) { + if (!parent[childrenAttr]) { + parent[childrenAttr] = [] + } + if($.inArray(entry, parent[childrenAttr])< 0) + parent[childrenAttr].push(entry) + inTree = true + } + }) + if(!inTree){ + tree.push(entry) + } + } + }) + return tree + } + + function renderTree(tree, childrenAttr, idAttr, attrs, renderer, tableAttributes) { + childrenAttr = childrenAttr || 'children' + idAttr = idAttr || 'id' + tableAttributes = tableAttributes || {} + var maxLevel = 0; + var ret = [] + + var table = $("") + $.each(tableAttributes, function(key, value){ + if(key == 'class' && value != 'jsTT') { + table.addClass(value) + } else { + table.attr(key, value) + } + }) + var thead = $("") + var tr = $("") + var tbody = $("") + + table.append(thead) + thead.append(tr) + table.append(tbody) + if (attrs) { + $.each(attrs, function(attr, desc) { + $(tr).append($('')) + }) + } else { + $(tr).append($('')) + $.each(tree[0], function(key, value) { + if (key != childrenAttr && key != idAttr) + $(tr).append($('')) + }) + } + + function render(node, parent) { + var tr = $("") + $(tr).attr('data-tt-id', node[idAttr]) + $(tr).attr('data-tt-level', node['data-tt-level']) + if(!node[childrenAttr] || node[childrenAttr].length == 0) + $(tr).attr('data-tt-isleaf', true) + else + $(tr).attr('data-tt-isnode', true) + if (parent) { + $(tr).attr('data-tt-parent-id', parent[idAttr]) + } + if (renderer) { + renderer($(tr), node) + }else if (attrs) { + $.each(attrs, function(attr, desc) { + $(tr).append($('')) + }) + } else { + $(tr).append($('')) + $.each(node, function(key, value) { + if (key != childrenAttr && key != idAttr && key != 'data-tt-level') + $(tr).append($('')) + }) + } + tbody.append(tr) + } + + function i_renderTree(subTree, childrenAttr, level, parent) { + maxLevel = Math.max(maxLevel, level) + $.each(subTree, function(i, node) { + node['data-tt-level'] = level + render(node, parent) + if (node[childrenAttr]) { + $.each(node[childrenAttr], function(i, child) { + i_renderTree([ child ], childrenAttr, level + 1, node) + }) + } + }) + } + i_renderTree(tree, childrenAttr, 1) + if (tree[0]) + tree[0].maxLevel = maxLevel + return table + } + + function attr2attr(nodes, attrs){ + $.each(nodes, function(i, node) { + $.each(attrs, function(j, at) { + node[at] = $(node).attr(at) + }) + }) + return nodes + } + + function treeTable(table){ + table.addClass('jsTT') + table.expandLevel = function (n) { + $("tr[data-tt-level]", table).each(function(index) { + var level = parseInt($(this).attr('data-tt-level')) + if (level > n-1) { + this.trCollapse(true) + } else if (level == n-1){ + this.trExpand(true) + } + }) + } + function getLevel(node){ + var level = node.attr('data-tt-level') + if(level != undefined ) return parseInt(level) + var parentID = node.attr('data-tt-parent-id') + if( parentID == undefined){ + return 0 + } else { + return getLevel($('tr[data-tt-id="'+parentID+'"]', table).first()) + 1 + } + } + $("tr[data-tt-id]", table).each(function(i,node){ + node = $(node) + node.attr('data-tt-level', getLevel(node)) + }) + var dat = $("tr[data-tt-level]", table).get() + $.each(dat, function(j, d) { + d.trChildrenVisible = true + d.trChildren = [] + }) + dat = attr2attr(dat, ['data-tt-id', 'data-tt-parent-id']) + dat = makeTree(dat, 'data-tt-id', 'data-tt-parent-id', 'trChildren') + + var imgExpand = "data:image/png;base64,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" + var imgCollapse = "data:image/png;base64,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" + $("tr[data-tt-level]", table).each(function(index, tr) { + var level = $(tr).attr('data-tt-level') + var td = $("td",tr).first() + if(tr.trChildren.length>0){ + td.prepend($('')) + } else { + td.prepend($('')) + } + td.prepend($('')) + td.css('white-space','nowrap') + tr.trExpand = function(changeState){ + if(this.trChildren.length < 1) return + if(changeState) { + this.trChildrenVisible = true + $('#state', this).get(0).src= imgCollapse + } + var doit = changeState || this.trChildrenVisible + $.each(this.trChildren, function(i, ctr) { + if(doit) $(ctr).css('display', 'table-row') + ctr.trExpand() + }) + } + tr.trCollapse = function(changeState){ + if(this.trChildren.length < 1) return + if(changeState) { + this.trChildrenVisible = false + $('#state', this).get(0).src= imgExpand + } + $.each(this.trChildren, function(i, ctr) { + $(ctr).css('display', 'none') + ctr.trCollapse() + }) + } + $(tr).click(function() { + this.trChildrenVisible ? this.trCollapse(true) : this.trExpand(true) + }) + }) + return table + } + + function appendTreetable(tree, options) { + function inALine(nodes) { + var tr = $('') + $.each(nodes, function(i, node){ + tr.append($('
' + desc + '' + idAttr + '' + key + '
' + node[attr] + '' + node[idAttr] + '' + value + '
').append(node)) + }) + return $('').append(tr) + + } + options = options || {} + options.idAttr = (options.idAttr || 'id') + options.childrenAttr = (options.childrenAttr || 'children') + var controls = (options.controls || []) + + if (!options.mountPoint) + options.mountPoint = $('body') + + if (options.depthFirst) + depthFirst(tree, options.depthFirst, options.childrenAttr) + var rendered = renderTree(tree, options.childrenAttr, options.idAttr, + options.renderedAttr, options.renderer, options.tableAttributes) + + treeTable(rendered) + if (options.replaceContent) { + options.mountPoint.html('') + } + var initialExpandLevel = options.initialExpandLevel ? parseInt(options.initialExpandLevel) : -1 + initialExpandLevel = Math.min(initialExpandLevel, tree[0].maxLevel) + rendered.expandLevel(initialExpandLevel) + if(options.slider){ + var slider = $('
') + slider.width('200px') + slider.slider({ + min : 1, + max : tree[0].maxLevel, + range : "min", + value : initialExpandLevel, + slide : function(event, ui) { + rendered.expandLevel(ui.value) + } + }) + controls = [slider].concat(options.controls) + } + + if(controls.length >0){ + options.mountPoint.append(inALine(controls)) + } + options.mountPoint.append(rendered) + return rendered + } + + return { + depthFirst : depthFirst, + makeTree : makeTree, + renderTree : renderTree, + attr2attr : attr2attr, + treeTable : treeTable, + appendTreetable : appendTreetable, + jsTreeTable : '1.0', + register : function(target){ + $.each(this, function(key, value){ if(key != 'register') target[key] = value}) + } + } +})(); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/appservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/appservice.js new file mode 100755 index 00000000..47705836 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/appservice.js @@ -0,0 +1,12 @@ + +var app = angular.module('sentinelDashboardApp'); + +app.service('AppService', ['$http', function ($http) { + this.getApps = function () { + return $http({ + // url: 'app/mock_infos', + url: 'app/briefinfos.json', + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/degradeservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/degradeservice.js new file mode 100755 index 00000000..c4db2078 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/degradeservice.js @@ -0,0 +1,63 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('DegradeService', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: 'degrade/rules.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + var param = { + id: rule.id, + resource: rule.resource, + limitApp: rule.limitApp, + count: rule.count, + timeWindow: rule.timeWindow, + grade: rule.grade, + app: rule.app, + ip: rule.ip, + port: rule.port + }; + return $http({ + url: '/degrade/new.json', + params: param, + method: 'GET' + }); + }; + + this.saveRule = function (rule) { + var param = { + id: rule.id, + resource: rule.resource, + limitApp: rule.limitApp, + grade: rule.grade, + count: rule.count, + timeWindow: rule.timeWindow, + }; + return $http({ + url: '/degrade/save.json', + params: param, + method: 'GET' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + return $http({ + url: '/degrade/delete.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/flowservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/flowservice.js new file mode 100755 index 00000000..12fd6eac --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/flowservice.js @@ -0,0 +1,73 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('FlowService', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: 'flow/rules.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + var param = { + resource: rule.resource, + limitApp: rule.limitApp, + grade: rule.grade, + count: rule.count, + strategy: rule.strategy, + refResource: rule.refResource, + controlBehavior: rule.controlBehavior, + warmUpPeriodSec: rule.warmUpPeriodSec, + maxQueueingTimeMs: rule.maxQueueingTimeMs, + app: rule.app, + ip: rule.ip, + port: rule.port + }; + + return $http({ + url: '/flow/new.json', + params: param, + method: 'GET' + }); + }; + + this.saveRule = function (rule) { + var param = { + id: rule.id, + resource: rule.resource, + limitApp: rule.limitApp, + grade: rule.grade, + count: rule.count, + strategy: rule.strategy, + refResource: rule.refResource, + controlBehavior: rule.controlBehavior, + warmUpPeriodSec: rule.warmUpPeriodSec, + maxQueueingTimeMs: rule.maxQueueingTimeMs, + }; + + return $http({ + url: '/flow/save.json', + params: param, + method: 'GET' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + + return $http({ + url: '/flow/delete.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/identityservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/identityservice.js new file mode 100755 index 00000000..926c0021 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/identityservice.js @@ -0,0 +1,30 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('IdentityService', ['$http', function ($http) { + + this.fetchIdentityOfMachine = function (ip, port, searchKey) { + var param = { + ip: ip, + port: port, + searchKey: searchKey + }; + return $http({ + url: 'resource/machineResource.json', + params: param, + method: 'GET' + }); + }; + this.fetchClusterNodeOfMachine = function (ip, port, searchKey) { + var param = { + ip: ip, + port: port, + type: 'cluster', + searchKey: searchKey + }; + return $http({ + url: 'resource/machineResource.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js new file mode 100755 index 00000000..807f06f8 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js @@ -0,0 +1,10 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('MachineService', ['$http', function ($http) { + this.getAppMachines = function (app) { + return $http({ + url: 'app/' + app + '/machines.json', + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/metricservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/metricservice.js new file mode 100755 index 00000000..8d8a38e0 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/metricservice.js @@ -0,0 +1,36 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('MetricService', ['$http', function ($http) { + + this.queryAppSortedIdentities = function (params) { + return $http({ + url: '/metric/queryTopResourceMetric.json', + params: params, + method: 'GET' + }); + }; + + this.queryByAppAndIdentity = function (params) { + return $http({ + url: '/metric/queryByAppAndResource.json', + params: params, + method: 'GET' + }); + }; + + this.queryByMachineAndIdentity = function (ip, port, identity, startTime, endTime) { + var param = { + ip: ip, + port: port, + identity: identity, + startTime: startTime.getTime(), + endTime: endTime.getTime() + }; + + return $http({ + url: '/metric/queryByAppAndResource.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/systemservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/systemservice.js new file mode 100755 index 00000000..d842d068 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/systemservice.js @@ -0,0 +1,72 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('SystemService', ['$http', function ($http) { + this.queryMachineRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: 'system/rules.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + var param = { + app: rule.app, + ip: rule.ip, + port: rule.port + }; + if (rule.grade === 0) {// avgLoad + param.avgLoad = rule.avgLoad; + } else if (rule.grade === 1) {// avgRt + param.avgRt = rule.avgRt; + } else if (rule.grade === 2) {// maxThread + param.maxThread = rule.maxThread; + } else if (rule.grade === 3) {// qps + param.qps = rule.qps; + } + + return $http({ + url: '/system/new.json', + params: param, + method: 'GET' + }); + }; + + this.saveRule = function (rule) { + var param = { + id: rule.id, + }; + if (rule.grade === 0) {// avgLoad + param.avgLoad = rule.avgLoad; + } else if (rule.grade === 1) {// avgRt + param.avgRt = rule.avgRt; + } else if (rule.grade === 2) {// maxThread + param.maxThread = rule.maxThread; + } else if (rule.grade === 3) {// qps + param.qps = rule.qps; + } + return $http({ + url: '/system/save.json', + params: param, + method: 'GET' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + + return $http({ + url: '/system/delete.json', + params: param, + method: 'GET' + }); + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/styles/main.css b/sentinel-dashboard/src/main/webapp/resources/app/styles/main.css new file mode 100755 index 00000000..9b16bf64 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/styles/main.css @@ -0,0 +1,1476 @@ +.browsehappy { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +body { + padding: 0; +} + +/* Everything but the jumbotron gets side spacing for mobile first views */ + +.header, +.marketing, +.footer { + padding-left: 15px; + padding-right: 15px; +} + + +/* Custom page header */ + +.header { + border-bottom: 1px solid #e5e5e5; + margin-bottom: 10px; +} + + +/* Make the masthead heading the same height as the navigation */ + +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; + padding-bottom: 19px; +} + + +/* Custom page footer */ + +.footer { + padding-top: 19px; + color: #777; + border-top: 1px solid #e5e5e5; +} + +.container-narrow > hr { + margin: 30px 0; +} + + +/* Main marketing message and sign up button */ + +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + + +/* Supporting marketing content */ + +.marketing { + margin: 40px 0; +} + +.marketing p + h4 { + margin-top: 28px; +} + + +/* Responsive: Portrait tablets and up */ + +@media screen and (min-width: 768px) { + .container { + width: inherit; + margin-left: 60px; + margin-right: 5px; + } + /* Remove the padding we set earlier */ + .header, + .marketing, + .footer { + padding-left: 0; + padding-right: 0; + } + /* Space out the masthead */ + .header { + margin-bottom: 30px; + } + /* Remove the bottom border on the jumbotron for visual effect */ + .jumbotron { + border-bottom: 0; + } +} + +.navbar-inverse { + background-color: #1d9d74; + border-color: #1b926c; +} + +.navbar-inverse .navbar-nav > li > a { + color: #b0ddce; + font-size: 15px; +} + +.navbar-inverse .navbar-nav>.open>a, +.navbar-inverse .navbar-nav>.open>a:focus, +.navbar-inverse .navbar-nav>.open>a:hover { + background-color: #1b926c; +} + +@media (min-width: 900px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: 0%; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} + +.dropdown-menu { + min-width: 100px !important; +} + +.nav-sidebar li.active a { + background: #DDD; +} + +.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus { + background: #1d9d74; + /*background: #d9d9d9;*/ + color: white; +} + +.broadcast-message, +.broadcast-message-preview { + padding: 10px; + text-align: center; + background: #555; + color: #BBB; + margin-top: 50px; +} + +.card { + position: relative; + border: 1px solid #d9d9d9; + border-radius: 3px; + color: #666; + background-color: #fff; + width: 100%; + border-radius: 5px; +} + +.card .card-header { + padding: 9px 0; + height: 40px; + background: #555; + color: #fff; + text-align: center; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.card .card-body { + padding: 12px 10px; +} + +.card .card-footer { + height: 20px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .detail-brand { + float: left; + width: 30%; + line-height: 98px; + font-size: 30px; + text-align: center; + color: white; +} + +.card .default { + background: #1d9d74; +} + +.card .info { + background: #6EBEE7; +} + +.card .warn { + background: #ED7F54; +} + +.card .danger { + background: #6583BE; +} + +.card .detail .text-default { + color: #1d9d74; +} + +.card .detail .text-info { + color: #6EBEE7; +} + +.card .detail .text-warn { + color: #ED7F54; +} + +.card .detail .text-danger { + color: #6583BE; +} + +.card .detail { + float: right; + width: 70%; + line-height: 98px; + text-align: center; +} + +.card .detail .text { + font-size: 12px; +} + +.card .detail .number { + font-size: 30px; + font-weight: 500; +} + +.h100 { + height: 100px; +} + +.inline { + display: inline; +} + +.separator { + height: 1px; + background-color: #e5e5e5; + margin-top: 10px; +} + +.card > .card-body > table > thead > tr > td, +.card > .card-body > table > tbody > tr > td { + word-wrap: break-word; + word-break: break-all; +} + +.card > .card-body > table > thead > tr > td { + font-weight: 500; + font-size: 13px; + text-align: center; +} + +.card > .card-body > table > thead > tr > td > span { + font-weight: 500; + font-size: 10px; +} + +.card > .card-body > table > tbody > tr > td { + font-size: 12px; + text-align: center; +} + +.card > .card-body > table > tbody > tr > td > a { + color: #666; +} + +.thumbnails > .card > .card-body > table > thead > tr > td, +.thumbnails > .card > .card-body > table > tbody > tr > td { + font-size: 12px; + color: #777; + word-wrap: break-word; + word-break: break-all; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(n+2) { + text-align: center; +} + +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(n+2) { + font-weight: 700; + text-align: center; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(1), +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(1) { + text-align: left; +} + +.tools-header { + background: whitesmoke; + padding: 9px 0; + height: 40px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.tools-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.tools-header .brand > a { + color: #666; +} + +.tools-header > button, +.tools-header > select, +.tools-header > a { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 25px; + padding: 0 10px; + line-height: 25px; + color: #666; +} + +.tools-header .paged { + margin-right: 0px; +} + +.btn { + height: 32px; +} + +.btn.btn-main { + color: #ffffff; + background-color: #337ab7; + border-color: #337ab7; +} + +.btn:focus, +.btn:active { + outline: none !important; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + + +.btn.btn-danger-tag { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; + line-height: 1px; + font-size: 11px; + padding: 4px 4px; +} + +.btn.btn-danger { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn.btn-danger:hover, +.btn.btn-danger:focus, +.btn.btn-danger:active { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.form-control { + height : 32px; +} + +.form-control:focus { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + +.form-control { + border-radius: 8px; +} + +.input-label:before { + display: inline-block; + content: "*"; + color: #f44336; + font-family: SimSun; + font-size: 12px; + -webkit-transform: TranslateX(-10px); + -ms-transform: TranslateX(-10px); + transform: TranslateX(-10px); +} + +.label.label-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.badge-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + color: #555; + vertical-align: middle; + border-radius: 4px; + /* max-width: 100%; */ + width: 85%; + height: 100px; + line-height: 20px; + cursor: text; +} + +.bootstrap-tagsinput > .dropdown-menu { + min-width: 40px; + font-size: 12px; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + background-color: #1d9d74; + background-image: -webkit-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -o-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#1d9d74), to(#1d9d74)); + background-image: linear-gradient(to bottom, #1d9d74 0, #1d9d74 100%); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#1d9d74', endColorstr='#1d9d74', GradientType=0); + background-repeat: repeat-x; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + color: #fff; + text-decoration: none; + background-color: #1d9d74; + outline: 0; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus { + color: white; + text-decoration: none; + outline: 0; + background-color: #1d9d74; +} + +.inputs-header { + padding: 9px 0; + height: 50px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.inputs-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.inputs-header .brand > a { + color: #666; +} + +.inputs-header > input { + float: right; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; +} + +.inputs-header > a { + float: right; + margin: 1px 10px; + height: 30px; + padding: 5 5px; +} + +.inputs-header > select { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; + height: 25px; + font-size: 12px; +} + +.witdh-150 { + max-width: 150px; +} + +.witdh-200 { + max-width: 200px; +} + +.witdh-300 { + max-width: 300px; +} + +.card.highlight { + border-color: #d9534f; +} + +.card .pagination-footer { + height: 40px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .pagination-footer .tools { + font-size: 12px; + margin: 11px 0; + float: right; + display: inline; + margin-right: 20px; +} + +.card > .pagination-footer > .tools > span > input { + height: 25px; + max-width: 50px; + display: inline; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 8px 0; + float: right; + border-radius: 4px; +} + + +.pagination > a { + margin-right: 5px; + height: 28px; + width: 28px; + padding: 5px 0px; +} + +.pagination > .btn.active { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + + + + +.datepicker > .table > thead > tr > td, .datepicker > .table > tbody > tr > td, +.timepicker > .table > thead > tr > td, .timepicker > .table > tbody > tr > td { + padding: 5px 3px; +} + +.datepicker > .table > thead > tr > td > .btn, .datepicker > .table > tbody > tr > td > .btn, +.timepicker > .table > thead > tr > td > .btn, .timepicker > .table > tbody > tr > td > .btn { + border: 1px solid #FFFDFD; +} + +.datepicker > .table > thead > tr > td > .btn-default:hover, +.datepicker > .table > thead > tr > td > .btn-default:focus, +.datepicker > .table > thead > tr > td > .btn-default:active, +.datepicker > .table > tbody > tr > td > .btn-default:hover, +.datepicker > .table > tbody > tr > td > .btn-default:focus, +.datepicker > .table > tbody > tr > td > .btn-default:active, +.timepicker > .table > thead > tr > td > .btn-default:hover, +.timepicker > .table > thead > tr > td > .btn-default:focus, +.timepicker > .table > thead > tr > td > .btn-default:active, +.timepicker > .table > tbody > tr > td > .btn-default:hover, +.timepicker > .table > tbody > tr > td > .btn-default:focus, +.timepicker > .table > tbody > tr > td > .btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.datepicker > .table > thead > tr > td > a, .datepicker > .table > tbody > tr > td > a, +.timepicker > .table > thead > tr > td > a, .timepicker > .table > tbody > tr > td > a { + height: 25px; + width: 25px; + padding: 3px 0px; +} + +.datepicker > .table > tbody > tr:first-child > td > a { + padding: 4px 0px; +} + +.datepicker > .table > thead > tr > td > a.btn.active, +.datepicker > .table > tbody > tr > td > a.btn.active, +.timepicker > .table > thead > tr > td > a.btn.active, +.timepicker > .table > tbody > tr > td > a.btn.active { +/* color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74;*/ + color: #1d9d74; + border-color: #1d9d74; + background: white; + box-shadow: inset 0 0px 0px rgba(0,0,0,0.125); +} + +.datepicker > .table > thead > tr > td:not(:first-child):last-child > a, +.timepicker > .table > thead > tr > td:not(:first-child):last-child > a { + height: 25px; + width: 50px; + padding: 5px 0px; +} + +.datepicker > .table > tbody > tr > td > a, +.timepicker > .table > tbody > tr > td > a { + margin-left: 8px; +} + + +.selectize-input-200 > .selectize-input { + min-width: 250px; +} + +.highlight-border { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +}.browsehappy { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +body { + padding: 0; +} + + +/* Everything but the jumbotron gets side spacing for mobile first views */ + +.header, +.marketing, +.footer { + padding-left: 15px; + padding-right: 15px; +} + + +/* Custom page header */ + +.header { + border-bottom: 1px solid #e5e5e5; + margin-bottom: 10px; +} + + +/* Make the masthead heading the same height as the navigation */ + +.header h3 { + margin-top: 0; + margin-bottom: 0; + line-height: 40px; + padding-bottom: 19px; +} + + +/* Custom page footer */ + +.footer { + padding-top: 19px; + color: #777; + border-top: 1px solid #e5e5e5; +} + +.container-narrow > hr { + margin: 30px 0; +} + + +/* Main marketing message and sign up button */ + +.jumbotron { + text-align: center; + border-bottom: 1px solid #e5e5e5; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + + +/* Supporting marketing content */ + +.marketing { + margin: 40px 0; +} + +.marketing p + h4 { + margin-top: 28px; +} + + +/* Responsive: Portrait tablets and up */ + +@media screen and (min-width: 768px) { + .container { + width: inherit; + margin-left: 60px; + margin-right: 5px; + } + /* Remove the padding we set earlier */ + .header, + .marketing, + .footer { + padding-left: 0; + padding-right: 0; + } + /* Space out the masthead */ + .header { + margin-bottom: 30px; + } + /* Remove the bottom border on the jumbotron for visual effect */ + .jumbotron { + border-bottom: 0; + } +} + +.navbar-inverse { + background-color: #1d9d74; + border-color: #1b926c; +} + +.navbar-inverse .navbar-nav > li > a { + color: #b0ddce; + font-size: 15px; +} + +.navbar-inverse .navbar-nav>.open>a, +.navbar-inverse .navbar-nav>.open>a:focus, +.navbar-inverse .navbar-nav>.open>a:hover { + background-color: #1b926c; +} + +@media (min-width: 900px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: 0%; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} + +.dropdown-menu { + min-width: 100px !important; +} + +.nav-sidebar li.active a { + background: #DDD; +} + +.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus { + background: #1d9d74; + /*background: #d9d9d9;*/ + color: white; +} + +.broadcast-message, +.broadcast-message-preview { + padding: 10px; + text-align: center; + background: #555; + color: #BBB; + margin-top: 50px; +} + +.card { + position: relative; + border: 1px solid #d9d9d9; + border-radius: 3px; + color: #666; + background-color: #fff; + width: 100%; + border-radius: 5px; +} + +.card .card-header { + padding: 9px 0; + height: 40px; + background: #555; + color: #fff; + text-align: center; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.card .card-body { + padding: 12px 10px; +} + +.card .card-footer { + height: 20px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .detail-brand { + float: left; + width: 30%; + line-height: 98px; + font-size: 30px; + text-align: center; + color: white; +} + +.card .default { + background: #1d9d74; +} + +.card .info { + background: #6EBEE7; +} + +.card .warn { + background: #ED7F54; +} + +.card .danger { + background: #6583BE; +} + +.card .detail .text-default { + color: #1d9d74; +} + +.card .detail .text-info { + color: #6EBEE7; +} + +.card .detail .text-warn { + color: #ED7F54; +} + +.card .detail .text-danger { + color: #6583BE; +} + +.card .detail { + float: right; + width: 70%; + line-height: 98px; + text-align: center; +} + +.card .detail .text { + font-size: 12px; +} + +.card .detail .number { + font-size: 30px; + font-weight: 500; +} + +.h100 { + height: 100px; +} + +.inline { + display: inline; +} + +.separator { + height: 1px; + background-color: #e5e5e5; + margin-top: 10px; +} + +.card > .card-body > table > thead > tr > td, +.card > .card-body > table > tbody > tr > td { + word-wrap: break-word; + word-break: break-all; +} + +.card > .card-body > table > thead > tr > td { + font-weight: 500; + font-size: 13px; + text-align: center; +} + +.card > .card-body > table > thead > tr > td > span { + font-weight: 500; + font-size: 10px; +} + +.card > .card-body > table > tbody > tr > td { + font-size: 12px; + text-align: center; +} + +.card > .card-body > table > tbody > tr > td > a { + color: #666; +} + +.thumbnails > .card > .card-body > table > thead > tr > td, +.thumbnails > .card > .card-body > table > tbody > tr > td { + font-size: 12px; + color: #777; + word-wrap: break-word; + word-break: break-all; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(n+2) { + text-align: center; +} + +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(n+2) { + font-weight: 700; + text-align: center; +} + +.thumbnails > .card > .card-body > table > thead > tr > td:nth-child(1), +.thumbnails > .card > .card-body > table > tbody > tr > td:nth-child(1) { + text-align: left; +} + +.tools-header { + background: whitesmoke; + padding: 9px 0; + height: 40px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.tools-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.tools-header .brand > a { + color: #666; +} + +.tools-header > button, +.tools-header > select, +.tools-header > a { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 25px; + padding: 0 10px; + line-height: 25px; + color: #666; +} + +.tools-header .paged { + margin-right: 0px; +} + +.btn.btn-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.btn:focus, +.btn:active { + outline: none !important; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.btn-default-inverse { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.btn-default-inverse:hover, +.btn-default-inverse:focus, +.btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.btn.btn-danger-tag { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; + line-height: 1px; + font-size: 11px; + padding: 4px 4px; +} + +.btn.btn-danger { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn.btn-danger:hover, +.btn.btn-danger:focus, +.btn.btn-danger:active { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.form-control { + height : 32px; +} + +.form-control:focus { + border-color: #1d9d74; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + +.form-control { + border-radius: 8px; +} + +.input-label:before { + display: inline-block; + content: "*"; + color: #f44336; + font-family: SimSun; + font-size: 12px; + -webkit-transform: TranslateX(-10px); + -ms-transform: TranslateX(-10px); + transform: TranslateX(-10px); +} + +.label.label-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.badge-main { + color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74; +} + +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + color: #555; + vertical-align: middle; + border-radius: 4px; + /* max-width: 100%; */ + width: 85%; + height: 100px; + line-height: 20px; + cursor: text; +} + +.bootstrap-tagsinput > .dropdown-menu { + min-width: 40px; + font-size: 12px; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + background-color: #1d9d74; + background-image: -webkit-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -o-linear-gradient(top, #1d9d74 0, #1d9d74 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#1d9d74), to(#1d9d74)); + background-image: linear-gradient(to bottom, #1d9d74 0, #1d9d74 100%); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#1d9d74', endColorstr='#1d9d74', GradientType=0); + background-repeat: repeat-x; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover { + color: #fff; + text-decoration: none; + background-color: #1d9d74; + outline: 0; +} + +.bootstrap-tagsinput > .dropdown-menu>.active>a, +.bootstrap-tagsinput > .dropdown-menu>.active>a:hover, +.bootstrap-tagsinput > .dropdown-menu>.active>a:focus { + color: white; + text-decoration: none; + outline: 0; + background-color: #1d9d74; +} + +.inputs-header { + padding: 9px 0; + height: 50px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.inputs-header .brand { + font-size: 13px; + margin: 2px 10px; + font-weight: 700; + float: left; +} + +.inputs-header .brand > a { + color: #666; +} + +.inputs-header > input { + float: right; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; +} + +.inputs-header > a { + float: right; + margin: 1px 10px; + height: 30px; + padding: 5 5px; +} + +.inputs-header > select { + float: right; + max-width: 80px; + margin: 1px 10px; + height: 30px; + padding: 0 10px; + color: #666; + height: 25px; + font-size: 12px; +} + +.witdh-150 { + max-width: 150px; +} + +.witdh-200 { + max-width: 200px; +} + +.card.highlight { + border-color: #d9534f; +} + +.card .pagination-footer { + height: 40px; + font-size: 10px; + color: #777; + margin-top: -15px; + margin-bottom: 5px; + margin-left: 20px; + margin-right: 20px; +} + +.card .pagination-footer .tools { + font-size: 12px; + margin: 11px 0; + float: right; + display: inline; + margin-right: 20px; +} + +.card > .pagination-footer > .tools > span > input { + height: 25px; + max-width: 50px; + display: inline; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 8px 0; + float: right; + border-radius: 4px; +} + + +.pagination > a { + margin-right: 5px; + height: 28px; + width: 28px; + padding: 5px 0px; +} + +.pagination > .btn.active { + color: #ffffff; + background-color: #449d44; + border-color: #449d44; +} + + + + +.datepicker > .table > thead > tr > td, .datepicker > .table > tbody > tr > td, +.timepicker > .table > thead > tr > td, .timepicker > .table > tbody > tr > td { + padding: 5px 3px; +} + +.datepicker > .table > thead > tr > td > .btn, .datepicker > .table > tbody > tr > td > .btn, +.timepicker > .table > thead > tr > td > .btn, .timepicker > .table > tbody > tr > td > .btn { + border: 1px solid #FFFDFD; +} + +.datepicker > .table > thead > tr > td > .btn-default:hover, +.datepicker > .table > thead > tr > td > .btn-default:focus, +.datepicker > .table > thead > tr > td > .btn-default:active, +.datepicker > .table > tbody > tr > td > .btn-default:hover, +.datepicker > .table > tbody > tr > td > .btn-default:focus, +.datepicker > .table > tbody > tr > td > .btn-default:active, +.timepicker > .table > thead > tr > td > .btn-default:hover, +.timepicker > .table > thead > tr > td > .btn-default:focus, +.timepicker > .table > thead > tr > td > .btn-default:active, +.timepicker > .table > tbody > tr > td > .btn-default:hover, +.timepicker > .table > tbody > tr > td > .btn-default:focus, +.timepicker > .table > tbody > tr > td > .btn-default:active { + color: #1d9d74; + border-color: #1d9d74; + background: white; +} + +.datepicker > .table > thead > tr > td > a, .datepicker > .table > tbody > tr > td > a, +.timepicker > .table > thead > tr > td > a, .timepicker > .table > tbody > tr > td > a { + height: 25px; + width: 25px; + padding: 3px 0px; +} + +.datepicker > .table > tbody > tr:first-child > td > a { + padding: 4px 0px; +} + +.datepicker > .table > thead > tr > td > a.btn.active, +.datepicker > .table > tbody > tr > td > a.btn.active, +.timepicker > .table > thead > tr > td > a.btn.active, +.timepicker > .table > tbody > tr > td > a.btn.active { +/* color: #ffffff; + background-color: #1d9d74; + border-color: #1d9d74;*/ + color: #1d9d74; + border-color: #1d9d74; + background: white; + box-shadow: inset 0 0px 0px rgba(0,0,0,0.125); +} + +.datepicker > .table > thead > tr > td:not(:first-child):last-child > a, +.timepicker > .table > thead > tr > td:not(:first-child):last-child > a { + height: 25px; + width: 50px; + padding: 5px 0px; +} + +.datepicker > .table > tbody > tr > td > a, +.timepicker > .table > tbody > tr > td > a { + margin-left: 8px; +} + + +.selectize-input-200 > .selectize-input { + min-width: 250px; +} + +.highlight-border { + border-color: #1d9d74; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + + +.sortorder:after { + content: '\25b2'; +} +.sortorder.reverse:after { + content: '\25bc'; +} + + + +.input-control select { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + position: relative; + border: 1px #d9d9d9 solid; + width: 100%; + height: 100%; + padding: .3125rem; + z-index: 0; +} + +.navbar-inverse { + background-color: #337ab7; + border-color: #337ab7; +} + +.sidebar { + z-index: 1; + width: 220px; + /*position: fixed;*/ + top: 0; + left: 0; + height: 100%; +} + +#page-wrapper { + position: inherit; + margin: 70px 0 0 220px; + padding: 12px 30px; + border-left: 0px solid #e7e7e7; +} + +.sidebar .sidebar-nav.navbar-collapse { + padding-right: 0; + padding-left: 0; + background-color: #F5F5F5; + position: relative; + color: black; + width: 100%; + padding: 0; + margin: 0; + list-style: none inside none; +} + +.sidebar a { + color: #555; +} + +.sidebar ul li:hover { + color:red; +} + +.form-control { + border-radius: 8px; +} + +.form-control:focus { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +} + +.highlight-border { + border-color: #337ab7; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(29, 157, 116, 1); +}.browsehappy { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +.btn.btn-main { + color: #ffffff; + background-color: #337ab7; + border-color: #337ab7; +} + +.btn-default-inverse { + color: #337ab7; + border-color: #337ab7; + background: white; +} + +.btn-default-inverse:hover, +.btn-default-inverse:focus, +.btn-default:active { + color: #337ab7; + border-color: #337ab7; + background: white; +} + +.btn-danger-inverse { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.btn-danger-inverse:hover, +.btn-danger-inverse:focus, +.btn-danger:active { + color: #d9534f; + border-color: #d9534f; + background: white; +} + +.btn-tab-active, +.btn-tab-active:hover, +.btn-tab-active:focus, +.btn-tab-default:hover, +.btn-tab-default:focus, +.btn-tab-default:active { + color: #337ab7; + border-color: #337ab7; + background: white; + font-weight: 600; +} +.btn-tab-default { + color: #777; + background: white; + font-weight: 600; +} + +.pagination > .btn.active { + color: #ffffff; + background-color: #337ab7; + border-color: #337ab7; +} + +.btn-default:hover, .btn-default:focus, .btn-default:active { + color: #337ab7; + border-color: #337ab7; + background: white; +} + +.bootstrap-switch.bootstrap-switch-on { + border-color: #337ab7; +} + +.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success, .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success { + color: #fff; + background: #337ab7; +} + +.selectize-input-200 > .selectize-input { + min-width: 200px; + border-color: #337ab7; +} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/styles/page.css b/sentinel-dashboard/src/main/webapp/resources/app/styles/page.css new file mode 100755 index 00000000..af3ba447 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/styles/page.css @@ -0,0 +1,399 @@ +/*! + * Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com) + * Code licensed under the Apache License v2.0. + * For details, see http://www.apache.org/licenses/LICENSE-2.0. + */ + +body { + background-color: #f8f8f8; +} + +.example { + padding: .625rem 1.825rem .625rem 2.5rem; + border: 1px #ccc dashed; + position: relative; + margin: 0 0 .625rem 0; + background-color: #ffffff; +} + +dl dt, +dl dd { + line-height: 1.25rem; +} +dl dt { + font-style: normal; + font-weight: 700; +} +dl dd { + margin-left: .9375rem; +} +dl.horizontal dt { + float: left; + width: 10rem; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} +dl.horizontal dd { + margin-left: 11.25rem; +} + +#wrapper { + width: 100%; +} + +#page-wrapper { + padding: 0 15px; + min-height: 568px; + background-color: #fff; +} + +@media(min-width:768px) { + #page-wrapper { + position: inherit; + margin: 0 0 0 250px; + padding: 0 30px; + border-left: 1px solid #e7e7e7; + } +} + +.navbar-top-links { + margin-right: 0; +} + +.navbar-top-links li { + display: inline-block; +} + +.navbar-top-links li:last-child { + margin-right: 15px; +} + +.navbar-top-links li a { + padding: 15px; + min-height: 50px; +} + +.navbar-top-links .dropdown-menu li { + display: block; +} + +.navbar-top-links .dropdown-menu li:last-child { + margin-right: 0; +} + +.navbar-top-links .dropdown-menu li a { + padding: 3px 20px; + min-height: 0; +} + +.navbar-top-links .dropdown-menu li a div { + white-space: normal; +} + +.navbar-top-links .dropdown-messages, +.navbar-top-links .dropdown-tasks, +.navbar-top-links .dropdown-alerts { + width: 310px; + min-width: 0; +} + +.navbar-top-links .dropdown-messages { + margin-left: 5px; +} + +.navbar-top-links .dropdown-tasks { + margin-left: -59px; +} + +.navbar-top-links .dropdown-alerts { + margin-left: -123px; +} + +.navbar-top-links .dropdown-user { + right: 0; + left: auto; +} + +.sidebar .sidebar-nav.navbar-collapse { + padding-right: 0; + padding-left: 0; + background-color: #71b1d1; + color: #ffffff; + position: relative; + width: 100%; + padding: 0; + margin: 0; + list-style: none inside none; +} + +.sidebar .sidebar-search { + padding: 15px; +} + +.sidebar ul li { + border-bottom: 1px solid #e7e7e7; +} + +.sidebar ul li a.active { + background-color: #ffffff; + color: #ffffff; +} + +.sidebar a{ + color: #fff; +} + +.sidebar .arrow { + float: right; +} + +.sidebar .fa.arrow:before { + content: "\f104"; +} + +.sidebar .active>a>.fa.arrow:before { + content: "\f107"; +} + +.sidebar .nav-second-level li, +.sidebar .nav-third-level li { + border-bottom: 0!important; +} + +.sidebar .nav-second-level li a { + padding-left: 37px; +} + +.sidebar .nav-third-level li a { + padding-left: 52px; +} + +@media(min-width:768px) { + .sidebar { + z-index: 1; + position: absolute; + width: 250px; + margin-top: 51px; + } + + .navbar-top-links .dropdown-messages, + .navbar-top-links .dropdown-tasks, + .navbar-top-links .dropdown-alerts { + margin-left: auto; + } +} + + +.btn-outline { + color: inherit; + background-color: transparent; + transition: all .5s; +} + +.btn-primary.btn-outline { + color: #428bca; +} + +.btn-success.btn-outline { + color: #5cb85c; +} + +.btn-info.btn-outline { + color: #5bc0de; +} + +.btn-warning.btn-outline { + color: #f0ad4e; +} + +.btn-danger.btn-outline { + color: #d9534f; +} + +.btn-primary.btn-outline:hover, +.btn-success.btn-outline:hover, +.btn-info.btn-outline:hover, +.btn-warning.btn-outline:hover, +.btn-danger.btn-outline:hover { + color: #fff; +} + +.chat { + margin: 0; + padding: 0; + list-style: none; +} + +.chat li { + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px dotted #999; +} + +.chat li.left .chat-body { + margin-left: 60px; +} + +.chat li.right .chat-body { + margin-right: 60px; +} + +.chat li .chat-body p { + margin: 0; +} + +.panel .slidedown .glyphicon, +.chat .glyphicon { + margin-right: 5px; +} + +.chat-panel .panel-body { + height: 350px; + overflow-y: scroll; +} + +.login-panel { + margin-top: 25%; +} + +.flot-chart { + display: block; + height: 400px; +} + +.flot-chart-content { + width: 100%; + height: 100%; +} + +.dataTables_wrapper { + position: relative; + clear: both; +} + +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + background: 0 0; +} + +table.dataTable thead .sorting_asc:after { + content: "\f0de"; + float: right; + font-family: fontawesome; +} + +table.dataTable thead .sorting_desc:after { + content: "\f0dd"; + float: right; + font-family: fontawesome; +} + +table.dataTable thead .sorting:after { + content: "\f0dc"; + float: right; + font-family: fontawesome; + color: rgba(50,50,50,.5); +} + +.btn-circle { + width: 30px; + height: 30px; + padding: 6px 0; + border-radius: 15px; + text-align: center; + font-size: 12px; + line-height: 1.428571429; +} + +.btn-circle.btn-lg { + width: 50px; + height: 50px; + padding: 10px 16px; + border-radius: 25px; + font-size: 18px; + line-height: 1.33; +} + +.btn-circle.btn-xl { + width: 70px; + height: 70px; + padding: 10px 16px; + border-radius: 35px; + font-size: 24px; + line-height: 1.33; +} + +.show-grid [class^=col-] { + padding-top: 10px; + padding-bottom: 10px; + border: 1px solid #ddd; + background-color: #eee!important; +} + +.show-grid { + margin: 15px 0; +} + +.huge { + font-size: 40px; +} + +.panel-green { + border-color: #5cb85c; +} + +.panel-green .panel-heading { + border-color: #5cb85c; + color: #fff; + background-color: #5cb85c; +} + +.panel-green a { + color: #5cb85c; +} + +.panel-green a:hover { + color: #3d8b3d; +} + +.panel-red { + border-color: #d9534f; +} + +.panel-red .panel-heading { + border-color: #d9534f; + color: #fff; + background-color: #d9534f; +} + +.panel-red a { + color: #d9534f; +} + +.panel-red a:hover { + color: #b52b27; +} + +.panel-yellow { + border-color: #f0ad4e; +} + +.panel-yellow .panel-heading { + border-color: #f0ad4e; + color: #fff; + background-color: #f0ad4e; +} + +.panel-yellow a { + color: #f0ad4e; +} + +.panel-yellow a:hover { + color: #df8a13; +} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/styles/timeline.css b/sentinel-dashboard/src/main/webapp/resources/app/styles/timeline.css new file mode 100755 index 00000000..92161ebe --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/styles/timeline.css @@ -0,0 +1,180 @@ +.timeline { + position: relative; + padding: 20px 0 20px; + list-style: none; +} + +.timeline:before { + content: " "; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 3px; + margin-left: -1.5px; + background-color: #eeeeee; +} + +.timeline > li { + position: relative; + margin-bottom: 20px; +} + +.timeline > li:before, +.timeline > li:after { + content: " "; + display: table; +} + +.timeline > li:after { + clear: both; +} + +.timeline > li:before, +.timeline > li:after { + content: " "; + display: table; +} + +.timeline > li:after { + clear: both; +} + +.timeline > li > .timeline-panel { + float: left; + position: relative; + width: 46%; + padding: 20px; + border: 1px solid #d4d4d4; + border-radius: 2px; + -webkit-box-shadow: 0 1px 6px rgba(0,0,0,0.175); + box-shadow: 0 1px 6px rgba(0,0,0,0.175); +} + +.timeline > li > .timeline-panel:before { + content: " "; + display: inline-block; + position: absolute; + top: 26px; + right: -15px; + border-top: 15px solid transparent; + border-right: 0 solid #ccc; + border-bottom: 15px solid transparent; + border-left: 15px solid #ccc; +} + +.timeline > li > .timeline-panel:after { + content: " "; + display: inline-block; + position: absolute; + top: 27px; + right: -14px; + border-top: 14px solid transparent; + border-right: 0 solid #fff; + border-bottom: 14px solid transparent; + border-left: 14px solid #fff; +} + +.timeline > li > .timeline-badge { + z-index: 100; + position: absolute; + top: 16px; + left: 50%; + width: 50px; + height: 50px; + margin-left: -25px; + border-radius: 50% 50% 50% 50%; + text-align: center; + font-size: 1.4em; + line-height: 50px; + color: #fff; + background-color: #999999; +} + +.timeline > li.timeline-inverted > .timeline-panel { + float: right; +} + +.timeline > li.timeline-inverted > .timeline-panel:before { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; +} + +.timeline > li.timeline-inverted > .timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; +} + +.timeline-badge.primary { + background-color: #2e6da4 !important; +} + +.timeline-badge.success { + background-color: #3f903f !important; +} + +.timeline-badge.warning { + background-color: #f0ad4e !important; +} + +.timeline-badge.danger { + background-color: #d9534f !important; +} + +.timeline-badge.info { + background-color: #5bc0de !important; +} + +.timeline-title { + margin-top: 0; + color: inherit; +} + +.timeline-body > p, +.timeline-body > ul { + margin-bottom: 0; +} + +.timeline-body > p + p { + margin-top: 5px; +} + +@media(max-width:767px) { + ul.timeline:before { + left: 40px; + } + + ul.timeline > li > .timeline-panel { + width: calc(100% - 90px); + width: -moz-calc(100% - 90px); + width: -webkit-calc(100% - 90px); + } + + ul.timeline > li > .timeline-badge { + top: 16px; + left: 15px; + margin-left: 0; + } + + ul.timeline > li > .timeline-panel { + float: right; + } + + ul.timeline > li > .timeline-panel:before { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; + } + + ul.timeline > li > .timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; + } +} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/home.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/home.html new file mode 100755 index 00000000..1d169691 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/home.html @@ -0,0 +1,13 @@ +
+
+
+

欢迎使用Sentinel控制台

+
+ +
+ + +
+
+ +
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/main.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/main.html new file mode 100755 index 00000000..c5abed5d --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dashboard/main.html @@ -0,0 +1,10 @@ +
+ +
+ + +
+
+
+ +
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/degrade.html b/sentinel-dashboard/src/main/webapp/resources/app/views/degrade.html new file mode 100755 index 00000000..1f1b14bf --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/degrade.html @@ -0,0 +1,102 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ 降级规则 + + + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + + 降级应用 + + 阈值类型 + + 阈值 + + 时间窗口(s) + + 操作 +
{{rule.resource}}{{rule.limitApp }} + {{rule.grade==0 ? 'RT' : '异常比例'}} + + {{rule.count}} + + {{rule.timeWindow}} + + + +
+ + + + + + + + + + + + diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/confirm-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/confirm-dialog.html new file mode 100755 index 00000000..a233a71f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/confirm-dialog.html @@ -0,0 +1,20 @@ +
+ {{confirmDialog.title}} +
+
+
+

+ {{confirmDialog.attentionTitle}}: +
+
+ {{confirmDialog.attention}} +

+
+
+
+ + +
+
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html new file mode 100755 index 00000000..9cc64d58 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/degrade-rule-dialog.html @@ -0,0 +1,59 @@ +
+ {{degradeRuleDialog.title}} +
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  RT   +  异常比例 +
+
+
+ +
+ + +
+ + +
+ +
+ +
+
+
+
+
+
+ + + +
+
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html new file mode 100755 index 00000000..f3ef0e21 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/flow-rule-dialog.html @@ -0,0 +1,106 @@ +
+ {{flowRuleDialog.title}} +
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+
+  QPS   +  线程数 +
+
+ +
+ +
+
+ +
+
+ +
+
+  直接   +  关联   +  链路   +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+  快速失败   +  Warm Up   +  排队等待 +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ + + +
+
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html new file mode 100755 index 00000000..9ef48b7f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/system-rule-dialog.html @@ -0,0 +1,57 @@ +
+ {{systemRuleDialog.title}} +
+
+
+
+ +
+ +
+
+ +  LOAD   + +  RT   + +  线程数   + +  QPS +
+
+ +  LOAD   + +  RT   + +  线程数   + +  QPS +
+
+
+
+ +
+ + + + +
+
+
+
+
+
+ + +
+
+
+
diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/flow.html b/sentinel-dashboard/src/main/webapp/resources/app/views/flow.html new file mode 100755 index 00000000..36a48421 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/flow.html @@ -0,0 +1,111 @@ +
+
+ {{app}} +
+
+ +
+
+ +
+ +
+
+
+
+
+ 流控规则 + + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + + 流控应用 + + 流控模式 + + 阈值类型 + + 单机阈值 + + 流控方式 + + 操作 +
{{rule.resource}}{{rule.limitApp }} + 直接 + 关联 + 链路 + + {{rule.grade==0 ? '线程数' : 'QPS'}} + + {{rule.count}} + + 快速失败 + Warm Up + 排队等待 + + + +
+
+ + + +
+ +
+ +
+ +
+ diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/identity.html b/sentinel-dashboard/src/main/webapp/resources/app/views/identity.html new file mode 100755 index 00000000..682f71bb --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/identity.html @@ -0,0 +1,106 @@ +
+
+ {{app}} +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ 簇点链路 + + + +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 资源名 + 通过QPS拒绝QPS线程数平均RT分钟通过分钟拒绝操作
+ + + {{resource.resource}} + + {{resource.passQps}}{{resource.blockedQps}}{{resource.threadNum}}{{resource.averageRt}}{{resource.oneMinutePassed}} + {{resource.oneMinuteBlocked}} {{resource.oneMinuteBlocked}} +
+ + +
+
+
+ + + +
+ +
+ +
+ +
+ \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/machine.html b/sentinel-dashboard/src/main/webapp/resources/app/views/machine.html new file mode 100755 index 00000000..1e9fa5ab --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/machine.html @@ -0,0 +1,94 @@ + + + + + + + + + + + +
+
+ {{app}} +
+
+ + + + + +
+
+
+
+
+
+ 机器列表 + 实例总数 {{machines.length}}, 健康 {{healthCount}}, 失联 {{machines.length-healthCount}}. + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
+ 机器名 + + IP地址 + + 端口号 + + 健康状态 + + 心跳时间 +
{{entry.hostname}}{{entry.ip}} {{entry.port}} 健康失联{{entry.version | date: 'yyyy/MM/dd HH:mm:ss'}}
+
+ + + +
+ +
+ +
+ +
+ diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/metric.html b/sentinel-dashboard/src/main/webapp/resources/app/views/metric.html new file mode 100755 index 00000000..80f2c887 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/metric.html @@ -0,0 +1,117 @@ +
+
+ {{app}} +
+
+ +
+
+
+
+
+
+ + + + {{metricTypeDesc}} 实时监控 + + + + + +
+ + +
+
+
+
+ + +
+
+  {{metric.resource}} + + + +
+ + +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
时间p_qpsb_qpsrt(ms)
{{tableObj.timestamp | date: 'HH:mm:ss'}}{{tableObj.passedQps | number : 1}}{{tableObj.blockedQps | number : 1}}{{tableObj.rt | number : 1}}
-
+
+
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
  • +
    + + + +
    +
    + +
    + +
    + +
    diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/pagination.tpl.html b/sentinel-dashboard/src/main/webapp/resources/app/views/pagination.tpl.html new file mode 100755 index 00000000..6ebbee2f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/pagination.tpl.html @@ -0,0 +1,18 @@ + diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/system.html b/sentinel-dashboard/src/main/webapp/resources/app/views/system.html new file mode 100755 index 00000000..80fa9b29 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/system.html @@ -0,0 +1,90 @@ +
    +
    + {{app}} +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + 系统规则 + + + +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + +
    + 阈值类型 + + 单机阈值 + + 操作 +
    + LOAD + RT + 线程数 + QPS + + {{rule.avgLoad}} + {{rule.avgRt}} + {{rule.maxThread}} + {{rule.qps}} + + + +
    +
    + + + +
    + +
    + +
    + +
    + diff --git a/sentinel-dashboard/src/main/webapp/resources/dist/css/app.css b/sentinel-dashboard/src/main/webapp/resources/dist/css/app.css new file mode 100755 index 00000000..a46c32e3 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/dist/css/app.css @@ -0,0 +1,5 @@ +.chat,.timeline{list-style:none}#loading-bar,#loading-bar-spinner{pointer-events:none;-webkit-pointer-events:none;-webkit-transition:350ms linear all;-moz-transition:350ms linear all;-o-transition:350ms linear all;transition:350ms linear all}#loading-bar-spinner.ng-enter,#loading-bar-spinner.ng-leave.ng-leave-active,#loading-bar.ng-enter,#loading-bar.ng-leave.ng-leave-active{opacity:0}#loading-bar-spinner.ng-enter.ng-enter-active,#loading-bar-spinner.ng-leave,#loading-bar.ng-enter.ng-enter-active,#loading-bar.ng-leave{opacity:1}#loading-bar .bar{-webkit-transition:width 350ms;-moz-transition:width 350ms;-o-transition:width 350ms;transition:width 350ms;background:#29d;position:fixed;z-index:10002;top:0;left:0;width:100%;height:2px;border-bottom-right-radius:1px;border-top-right-radius:1px}#loading-bar .peg{position:absolute;width:70px;right:0;top:0;height:2px;opacity:.45;-moz-box-shadow:#29d 1px 0 6px 1px;-ms-box-shadow:#29d 1px 0 6px 1px;-webkit-box-shadow:#29d 1px 0 6px 1px;box-shadow:#29d 1px 0 6px 1px;-moz-border-radius:100%;-webkit-border-radius:100%;border-radius:100%}#loading-bar-spinner{display:block;position:fixed;z-index:10002;top:10px;left:10px}#loading-bar-spinner .spinner-icon{width:14px;height:14px;border:2px solid transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:loading-bar-spinner .4s linear infinite;-moz-animation:loading-bar-spinner .4s linear infinite;-ms-animation:loading-bar-spinner .4s linear infinite;-o-animation:loading-bar-spinner .4s linear infinite;animation:loading-bar-spinner .4s linear infinite}@-webkit-keyframes loading-bar-spinner{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes loading-bar-spinner{0%{-moz-transform:rotate(0);transform:rotate(0)}100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}@-o-keyframes loading-bar-spinner{0%{-o-transform:rotate(0);transform:rotate(0)}100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes loading-bar-spinner{0%{-ms-transform:rotate(0);transform:rotate(0)}100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-bar-spinner{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.bootstrap-switch{display:inline-block;direction:ltr;cursor:pointer;border-radius:4px;border:1px solid #ccc;position:relative;text-align:left;overflow:hidden;line-height:8px;z-index:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.bootstrap-switch .bootstrap-switch-container{display:inline-block;top:0;border-radius:4px;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;cursor:pointer;display:table-cell;vertical-align:middle;padding:6px 12px;font-size:14px;line-height:20px}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{color:#fff;background:#5bc0de}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f0ad4e;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{color:#fff;background:#d9534f}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{color:#000;background:#eee}.bootstrap-switch .bootstrap-switch-label{text-align:center;margin-top:-1px;margin-bottom:-1px;z-index:100;color:#333;background:#fff}.bootstrap-switch span::before{content:"\200b"}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:3px;border-top-left-radius:3px}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch input[type=radio],.bootstrap-switch input[type=checkbox]{position:absolute!important;top:0;left:0;margin:0;z-index:-1;opacity:0;filter:alpha(opacity=0);visibility:hidden}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{padding:1px 5px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{padding:5px 10px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{padding:6px 16px;font-size:18px;line-height:1.3333333}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default!important}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{opacity:.5;filter:alpha(opacity=50);cursor:default!important}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 3px 3px 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:3px 0 0 3px}.bootstrap-switch.bootstrap-switch-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:3px;border-top-left-radius:3px}.ngdialog,.ngdialog-overlay{position:fixed;top:0;right:0;bottom:0;left:0}@-webkit-keyframes ngdialog-fadeout{0%{opacity:1}100%{opacity:0}}@keyframes ngdialog-fadeout{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes ngdialog-fadein{0%{opacity:0}100%{opacity:1}}@keyframes ngdialog-fadein{0%{opacity:0}100%{opacity:1}}.ngdialog{box-sizing:border-box;overflow:auto;-webkit-overflow-scrolling:touch;z-index:10000}.ngdialog *,.ngdialog :after,.ngdialog :before{box-sizing:inherit}.ngdialog.ngdialog-disabled-animation,.ngdialog.ngdialog-disabled-animation .ngdialog-content,.ngdialog.ngdialog-disabled-animation .ngdialog-overlay{-webkit-animation:none!important;animation:none!important}.ngdialog-overlay{background:rgba(0,0,0,.4);-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadein .5s;animation:ngdialog-fadein .5s}.ngdialog-no-overlay{pointer-events:none}.ngdialog.ngdialog-closing .ngdialog-overlay{-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadeout .5s;animation:ngdialog-fadeout .5s}.ngdialog-content{background:#fff;-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadein .5s;animation:ngdialog-fadein .5s;pointer-events:all}.ngdialog.ngdialog-closing .ngdialog-content{-webkit-backface-visibility:hidden;-webkit-animation:ngdialog-fadeout .5s;animation:ngdialog-fadeout .5s}.ngdialog-close:before{font-family:Helvetica,Arial,sans-serif;content:'\00D7';cursor:pointer}body.ngdialog-open,html.ngdialog-open{overflow:hidden}@-webkit-keyframes ngdialog-flyin{0%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes ngdialog-flyin{0%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes ngdialog-flyout{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes ngdialog-flyout{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-40px);transform:translateY(-40px)}}.ngdialog.ngdialog-theme-default{padding-bottom:160px;padding-top:160px}.ngdialog.ngdialog-theme-default.ngdialog-closing .ngdialog-content{-webkit-animation:ngdialog-flyout .5s;animation:ngdialog-flyout .5s}.ngdialog.ngdialog-theme-default .ngdialog-content{-webkit-animation:ngdialog-flyin .5s;animation:ngdialog-flyin .5s;background:#f0f0f0;border-radius:5px;color:#444;font-family:Helvetica,sans-serif;font-size:1.1em;line-height:1.5em;margin:0 auto;max-width:100%;padding:1em;position:relative;width:450px}.ngdialog.ngdialog-theme-default .ngdialog-close{border-radius:5px;cursor:pointer;position:absolute;right:0;top:0}.ngdialog.ngdialog-theme-default .ngdialog-close:before{background:0 0;border-radius:3px;color:#bbb;content:'\00D7';font-size:26px;font-weight:400;height:30px;line-height:26px;position:absolute;right:3px;text-align:center;top:3px;width:30px}.ngdialog.ngdialog-theme-default .ngdialog-close:active:before,.ngdialog.ngdialog-theme-default .ngdialog-close:hover:before{color:#777}.ngdialog.ngdialog-theme-default .ngdialog-message{margin-bottom:.5em}.ngdialog.ngdialog-theme-default .ngdialog-input{margin-bottom:1em}.ngdialog.ngdialog-theme-default .ngdialog-input input[type=text],.ngdialog.ngdialog-theme-default .ngdialog-input input[type=password],.ngdialog.ngdialog-theme-default .ngdialog-input input[type=email],.ngdialog.ngdialog-theme-default .ngdialog-input input[type=url],.ngdialog.ngdialog-theme-default .ngdialog-input textarea{background:#fff;border:0;border-radius:3px;font-family:inherit;font-size:inherit;font-weight:inherit;margin:0 0 .25em;min-height:2.5em;padding:.25em .67em;width:100%}.ngdialog.ngdialog-theme-default .ngdialog-input input[type=text]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input input[type=password]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input input[type=email]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input input[type=url]:focus,.ngdialog.ngdialog-theme-default .ngdialog-input textarea:focus{box-shadow:inset 0 0 0 2px #8dbdf1;outline:0}.ngdialog.ngdialog-theme-default .ngdialog-buttons:after{content:'';display:table;clear:both}.ngdialog.ngdialog-theme-default .ngdialog-button{border:0;border-radius:3px;cursor:pointer;float:right;font-family:inherit;font-size:.8em;letter-spacing:.1em;line-height:1em;margin:0 0 0 .5em;padding:.75em 2em;text-transform:uppercase}.ngdialog.ngdialog-theme-default .ngdialog-button:focus{-webkit-animation:ngdialog-pulse 1.1s infinite;animation:ngdialog-pulse 1.1s infinite;outline:0}.btn:active,.btn:focus,.selectize-input>input:focus{outline:0!important}@media (max-width:568px){.ngdialog.ngdialog-theme-default .ngdialog-button:focus{-webkit-animation:none;animation:none}}.ngdialog.ngdialog-theme-default .ngdialog-button.ngdialog-button-primary{background:#3288e6;color:#fff}.ngdialog.ngdialog-theme-default .ngdialog-button.ngdialog-button-secondary{background:#e0e0e0;color:#777}.datetimepicker{border-radius:4px;direction:ltr;display:block;margin-top:1px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:320px}.datetimepicker>div{display:none}.datetimepicker .hour,.datetimepicker .minute{height:34px;line-height:34px;margin:0;width:25%}.datetimepicker .table{margin:0}.datetimepicker .table td,.datetimepicker .table th{border:0;border-radius:4px;height:20px;text-align:center}.datetimepicker .day:hover,.datetimepicker .hour:hover,.datetimepicker .left:hover,.datetimepicker .minute:hover,.datetimepicker .right:hover,.datetimepicker .switch:hover{background:#eee;cursor:pointer}.datetimepicker .disabled,.datetimepicker .disabled:hover{background:0 0;color:#ebebeb;cursor:default}.datetimepicker .active,.datetimepicker .active.disabled,.datetimepicker .active.disabled:hover,.datetimepicker .active:hover{background-color:#04c;background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;color:#fff;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#08c', endColorstr='#04c', GradientType=0);text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datetimepicker .current,.datetimepicker .current.disabled,.datetimepicker .current.disabled:hover,.datetimepicker .current:hover{background-color:#e5e5e5}.datetimepicker .active.active,.datetimepicker .active.disabled,.datetimepicker .active.disabled.active,.datetimepicker .active.disabled.disabled,.datetimepicker .active.disabled:active,.datetimepicker .active.disabled:hover,.datetimepicker .active.disabled:hover.active,.datetimepicker .active.disabled:hover.disabled,.datetimepicker .active.disabled:hover:active,.datetimepicker .active.disabled:hover:hover,.datetimepicker .active:active,.datetimepicker .active:hover,.datetimepicker .active:hover.active,.datetimepicker .active:hover.disabled,.datetimepicker .active:hover:active,.datetimepicker .active:hover:hover,.datetimepicker span.active.disabled:hover[disabled],.datetimepicker span.active.disabled[disabled],.datetimepicker span.active:hover[disabled],.datetimepicker span.active[disabled],.datetimepicker td.active.disabled:hover[disabled],.datetimepicker td.active.disabled[disabled],.datetimepicker td.active:hover[disabled],.datetimepicker td.active[disabled]{background-color:#04c}.datetimepicker span{border-radius:4px;cursor:pointer;display:block;float:left;height:54px;line-height:54px;margin:1%;width:23%}.datetimepicker span:hover{background:#eee}.datetimepicker .future,.datetimepicker .past{color:#999}.ui-notification{position:fixed;z-index:9999;width:300px;-webkit-transition:all ease .5s;-o-transition:all ease .5s;transition:all ease .5s;color:#fff;border-radius:0;background:#337ab7;box-shadow:5px 5px 10px rgba(0,0,0,.3)}.ui-notification.clickable{cursor:pointer}.ui-notification.clickable:hover{opacity:.7}.ui-notification.killed{-webkit-transition:opacity ease 1s;-o-transition:opacity ease 1s;transition:opacity ease 1s;opacity:0}.ui-notification>h3{font-size:14px;font-weight:700;display:block;margin:10px 10px 0;padding:0 0 5px;text-align:left;border-bottom:1px solid rgba(255,255,255,.3)}.ui-notification a{color:#fff}.ui-notification a:hover{text-decoration:underline}.ui-notification>.message{margin:10px}.ui-notification.warning{color:#fff;background:#f0ad4e}.ui-notification.error{color:#fff;background:#d9534f}.ui-notification.success{color:#fff;background:#5cb85c}.ui-notification.info{color:#fff;background:#5bc0de}table.rz-table{table-layout:fixed;border-collapse:collapse}table.rz-table th{position:relative;min-width:25px}table.rz-table th .rz-handle{width:10px;height:100%;position:absolute;top:0;right:0;cursor:ew-resize!important}table.rz-table th .rz-handle.rz-handle-active{border-right:1px dotted #000}.selectize-control.plugin-drag_drop.multi>.selectize-input>div.ui-sortable-placeholder{visibility:visible!important;background:#f2f2f2!important;background:rgba(0,0,0,.06)!important;border:0!important;-webkit-box-shadow:inset 0 0 12px 4px #fff;box-shadow:inset 0 0 12px 4px #fff}.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after{content:'!';visibility:hidden}.selectize-control.plugin-drag_drop .ui-sortable-helper{-webkit-box-shadow:0 2px 5px rgba(0,0,0,.2);box-shadow:0 2px 5px rgba(0,0,0,.2)}.selectize-dropdown-header{position:relative;padding:5px 8px;border-bottom:1px solid #d0d0d0;background:#f8f8f8;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0}.selectize-dropdown-header-close{position:absolute;right:8px;top:50%;color:#303030;opacity:.4;margin-top:-12px;line-height:20px;font-size:20px!important}.selectize-dropdown-header-close:hover{color:#000}.selectize-dropdown.plugin-optgroup_columns .optgroup{border-right:1px solid #f2f2f2;border-top:0 none;float:left;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.selectize-control.plugin-remove_button [data-value] .remove,.selectize-input{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;display:inline-block}.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child{border-right:0 none}.selectize-dropdown.plugin-optgroup_columns .optgroup:before{display:none}.selectize-dropdown.plugin-optgroup_columns .optgroup-header{border-top:0 none}.selectize-control.plugin-remove_button [data-value]{position:relative;padding-right:24px!important}.selectize-control.plugin-remove_button [data-value] .remove{z-index:1;position:absolute;top:0;right:0;bottom:0;width:17px;text-align:center;font-weight:700;font-size:12px;color:inherit;text-decoration:none;vertical-align:middle;padding:2px 0 0;border-left:1px solid #d0d0d0;-webkit-border-radius:0 2px 2px 0;-moz-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0;box-sizing:border-box}.selectize-control.plugin-remove_button [data-value] .remove:hover{background:rgba(0,0,0,.05)}.selectize-control.plugin-remove_button [data-value].active .remove{border-left-color:#cacaca}.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover{background:0 0}.selectize-control.plugin-remove_button .disabled [data-value] .remove{border-left-color:#fff}.selectize-control.plugin-remove_button .remove-single{position:absolute;right:0;top:0;font-size:23px}.selectize-control,.selectize-input{position:relative}.selectize-dropdown,.selectize-input,.selectize-input input{color:#303030;font-family:inherit;font-size:13px;line-height:18px;-webkit-font-smoothing:inherit}.selectize-control.single .selectize-input.input-active,.selectize-input{background:#fff;cursor:text;display:inline-block}.selectize-input{border:1px solid #d0d0d0;padding:8px;width:100%;overflow:hidden;z-index:1;box-sizing:border-box;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.1);box-shadow:inset 0 1px 1px rgba(0,0,0,.1);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.selectize-control.multi .selectize-input.has-items{padding:6px 8px 3px}.selectize-input.full{background-color:#fff}.selectize-input.disabled,.selectize-input.disabled *{cursor:default!important}.selectize-input.focus{-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.15);box-shadow:inset 0 1px 2px rgba(0,0,0,.15)}.selectize-input.dropdown-active{-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0}.selectize-input>*{vertical-align:baseline;display:-moz-inline-stack;display:inline-block;zoom:1}.selectize-control.multi .selectize-input>div{cursor:pointer;margin:0 3px 3px 0;padding:2px 6px;background:#f2f2f2;color:#303030;border:0 solid #d0d0d0}.selectize-control.multi .selectize-input>div.active{background:#e8e8e8;color:#303030;border:0 solid #cacaca}.selectize-control.multi .selectize-input.disabled>div,.selectize-control.multi .selectize-input.disabled>div.active{color:#7d7d7d;background:#fff;border:0 solid #fff}.selectize-input>input{display:inline-block!important;padding:0!important;min-height:0!important;max-height:none!important;max-width:100%!important;margin:0 2px 0 0!important;text-indent:0!important;border:0!important;background:0 0!important;line-height:inherit!important;-webkit-user-select:auto!important;-webkit-box-shadow:none!important;box-shadow:none!important}.selectize-input>input::-ms-clear{display:none}.selectize-input::after{content:' ';display:block;clear:left}.selectize-input.dropdown-active::before{content:' ';display:block;position:absolute;background:#f0f0f0;height:1px;bottom:0;left:0;right:0}.selectize-dropdown{position:absolute;z-index:10;border:1px solid #d0d0d0;background:#fff;margin:-1px 0 0;border-top:0 none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.1);-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px}.selectize-dropdown [data-selectable]{cursor:pointer;overflow:hidden}.selectize-dropdown [data-selectable] .highlight{background:rgba(125,168,208,.2);-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px}.selectize-dropdown .optgroup-header,.selectize-dropdown .option{padding:5px 8px}.selectize-dropdown .option,.selectize-dropdown [data-disabled],.selectize-dropdown [data-disabled] [data-selectable].option{cursor:inherit;opacity:.5}.selectize-dropdown [data-selectable].option{opacity:1}.selectize-dropdown .optgroup:first-child .optgroup-header{border-top:0 none}.selectize-dropdown .optgroup-header{color:#303030;background:#fff;cursor:default}.selectize-dropdown .active{background-color:#f5fafd;color:#495c68}.selectize-dropdown .active.create{color:#495c68}.selectize-dropdown .create{color:rgba(48,48,48,.5)}.selectize-dropdown-content{overflow-y:auto;overflow-x:hidden;max-height:200px;-webkit-overflow-scrolling:touch}.selectize-control.single .selectize-input,.selectize-control.single .selectize-input input{cursor:pointer}.selectize-control.single .selectize-input.input-active,.selectize-control.single .selectize-input.input-active input{cursor:text}.selectize-control.single .selectize-input:after{content:' ';display:block;position:absolute;top:50%;right:15px;margin-top:-3px;width:0;height:0;border-style:solid;border-width:5px 5px 0;border-color:grey transparent transparent}.selectize-control.single .selectize-input.dropdown-active:after{margin-top:-4px;border-width:0 5px 5px;border-color:transparent transparent grey}.selectize-control.rtl.single .selectize-input:after{left:15px;right:auto}.selectize-control.rtl .selectize-input>input{margin:0 4px 0 -2px!important}.selectize-control .selectize-input.disabled{opacity:.5;background-color:#fafafa}/*! + * Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com) + * Code licensed under the Apache License v2.0. + * For details, see http://www.apache.org/licenses/LICENSE-2.0. + */body{background-color:#f8f8f8}.example{padding:.625rem 1.825rem .625rem 2.5rem;border:1px dashed #ccc;position:relative;margin:0 0 .625rem;background-color:#fff}dl dd,dl dt{line-height:1.25rem}dl dt{font-style:normal;font-weight:700}dl dd{margin-left:.9375rem}dl.horizontal dt{float:left;width:10rem;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}dl.horizontal dd{margin-left:11.25rem}#wrapper{width:100%}#page-wrapper{min-height:568px;background-color:#fff}@media(min-width:768px){#page-wrapper{position:inherit;margin:0 0 0 250px;padding:0 30px;border-left:1px solid #e7e7e7}}.navbar-top-links{margin-right:0}.navbar-top-links li{display:inline-block}.flot-chart,.navbar-top-links .dropdown-menu li{display:block}.navbar-top-links li:last-child{margin-right:15px}.navbar-top-links li a{padding:15px;min-height:50px}.navbar-top-links .dropdown-menu li:last-child{margin-right:0}.navbar-top-links .dropdown-menu li a{padding:3px 20px;min-height:0}.navbar-top-links .dropdown-menu li a div{white-space:normal}.navbar-top-links .dropdown-alerts,.navbar-top-links .dropdown-messages,.navbar-top-links .dropdown-tasks{width:310px;min-width:0}.navbar-top-links .dropdown-messages{margin-left:5px}.navbar-top-links .dropdown-tasks{margin-left:-59px}.navbar-top-links .dropdown-alerts{margin-left:-123px}.navbar-top-links .dropdown-user{right:0;left:auto}.sidebar .sidebar-search{padding:15px}.sidebar ul li{border-bottom:1px solid #e7e7e7}.sidebar ul li a.active{background-color:#fff;color:#fff}.sidebar .arrow{float:right}.sidebar .fa.arrow:before{content:"\f104"}.sidebar .active>a>.fa.arrow:before{content:"\f107"}.sidebar .nav-second-level li,.sidebar .nav-third-level li{border-bottom:0!important}.sidebar .nav-second-level li a{padding-left:37px}.sidebar .nav-third-level li a{padding-left:52px}@media(min-width:768px){.sidebar{z-index:1;position:absolute;width:250px;margin-top:51px}.navbar-top-links .dropdown-alerts,.navbar-top-links .dropdown-messages,.navbar-top-links .dropdown-tasks{margin-left:auto}}.btn-outline{color:inherit;background-color:transparent;transition:all .5s}.btn-primary.btn-outline{color:#428bca}.btn-success.btn-outline{color:#5cb85c}.btn-info.btn-outline{color:#5bc0de}.btn-warning.btn-outline{color:#f0ad4e}.btn-danger.btn-outline{color:#d9534f}.btn-danger.btn-outline:hover,.btn-info.btn-outline:hover,.btn-primary.btn-outline:hover,.btn-success.btn-outline:hover,.btn-warning.btn-outline:hover{color:#fff}.chat{margin:0;padding:0}.chat li{margin-bottom:10px;padding-bottom:5px;border-bottom:1px dotted #999}.chat li.left .chat-body{margin-left:60px}.chat li.right .chat-body{margin-right:60px}.chat li .chat-body p{margin:0}.chat .glyphicon,.panel .slidedown .glyphicon{margin-right:5px}.chat-panel .panel-body{height:350px;overflow-y:scroll}.login-panel{margin-top:25%}.flot-chart{height:400px}.flot-chart-content{width:100%;height:100%}.dataTables_wrapper{position:relative;clear:both}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_desc_disabled{background:0 0}table.dataTable thead .sorting_asc:after{content:"\f0de";float:right;font-family:fontawesome}table.dataTable thead .sorting_desc:after{content:"\f0dd";float:right;font-family:fontawesome}table.dataTable thead .sorting:after{content:"\f0dc";float:right;font-family:fontawesome;color:rgba(50,50,50,.5)}.btn-circle{width:30px;height:30px;padding:6px 0;border-radius:15px;text-align:center;font-size:12px;line-height:1.428571429}.btn-circle.btn-lg{width:50px;height:50px;padding:10px 16px;border-radius:25px;font-size:18px;line-height:1.33}.btn-circle.btn-xl{width:70px;height:70px;padding:10px 16px;border-radius:35px;font-size:24px;line-height:1.33}.show-grid [class^=col-]{padding-top:10px;padding-bottom:10px;border:1px solid #ddd;background-color:#eee!important}.show-grid{margin:15px 0}.huge{font-size:40px}.panel-green{border-color:#5cb85c}.panel-green .panel-heading{border-color:#5cb85c;color:#fff;background-color:#5cb85c}.panel-green a{color:#5cb85c}.panel-green a:hover{color:#3d8b3d}.panel-red{border-color:#d9534f}.panel-red .panel-heading{border-color:#d9534f;color:#fff;background-color:#d9534f}.panel-red a{color:#d9534f}.panel-red a:hover{color:#b52b27}.panel-yellow{border-color:#f0ad4e}.panel-yellow .panel-heading{border-color:#f0ad4e;color:#fff;background-color:#f0ad4e}.panel-yellow a{color:#f0ad4e}.panel-yellow a:hover{color:#df8a13}.timeline{position:relative;padding:20px 0}.timeline:before{content:" ";position:absolute;top:0;bottom:0;left:50%;width:3px;margin-left:-1.5px;background-color:#eee}.timeline>li{position:relative;margin-bottom:20px}.timeline>li:after,.timeline>li:before{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-panel{float:left;position:relative;width:46%;padding:20px;border:1px solid #d4d4d4;border-radius:2px;-webkit-box-shadow:0 1px 6px rgba(0,0,0,.175);box-shadow:0 1px 6px rgba(0,0,0,.175)}.timeline>li>.timeline-panel:before{content:" ";display:inline-block;position:absolute;top:26px;right:-15px;border-top:15px solid transparent;border-right:0 solid #ccc;border-bottom:15px solid transparent;border-left:15px solid #ccc}.timeline>li>.timeline-panel:after{content:" ";display:inline-block;position:absolute;top:27px;right:-14px;border-top:14px solid transparent;border-right:0 solid #fff;border-bottom:14px solid transparent;border-left:14px solid #fff}.timeline>li>.timeline-badge{z-index:100;position:absolute;top:16px;left:50%;width:50px;height:50px;margin-left:-25px;border-radius:50%;text-align:center;font-size:1.4em;line-height:50px;color:#fff;background-color:#999}.timeline>li.timeline-inverted>.timeline-panel{float:right}.timeline>li.timeline-inverted>.timeline-panel:before{right:auto;left:-15px;border-right-width:15px;border-left-width:0}.timeline>li.timeline-inverted>.timeline-panel:after{right:auto;left:-14px;border-right-width:14px;border-left-width:0}.timeline-badge.primary{background-color:#2e6da4!important}.timeline-badge.success{background-color:#3f903f!important}.timeline-badge.warning{background-color:#f0ad4e!important}.timeline-badge.danger{background-color:#d9534f!important}.timeline-badge.info{background-color:#5bc0de!important}.timeline-title{margin-top:0;color:inherit}.timeline-body>p,.timeline-body>ul{margin-bottom:0}.timeline-body>p+p{margin-top:5px}@media(max-width:767px){ul.timeline:before{left:40px}ul.timeline>li>.timeline-panel{width:calc(100% - 90px);width:-moz-calc(100% - 90px);width:-webkit-calc(100% - 90px);float:right}ul.timeline>li>.timeline-badge{top:16px;left:15px;margin-left:0}ul.timeline>li>.timeline-panel:before{right:auto;left:-15px;border-right-width:15px;border-left-width:0}ul.timeline>li>.timeline-panel:after{right:auto;left:-14px;border-right-width:14px;border-left-width:0}}.header,.jumbotron{border-bottom:1px solid #e5e5e5}.btn{height:32px}.witdh-300{max-width:300px}body{padding:0}.footer,.header,.marketing{padding-left:15px;padding-right:15px}.header{margin-bottom:10px}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.card .detail,.card .detail-brand{line-height:98px;text-align:center}.footer{padding-top:19px;color:#777;border-top:1px solid #e5e5e5}.container-narrow>hr{margin:30px 0}.jumbotron{text-align:center}.jumbotron .btn{font-size:21px;padding:14px 24px}.marketing{margin:40px 0}.marketing p+h4{margin-top:28px}@media screen and (min-width:768px){.container{width:inherit;margin-left:60px;margin-right:5px}.footer,.header,.marketing{padding-left:0;padding-right:0}.header{margin-bottom:30px}.jumbotron{border-bottom:0}}.navbar-inverse .navbar-nav>li>a{color:#b0ddce;font-size:15px}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#1b926c}@media (min-width:900px){.navbar-right,.navbar-right~.navbar-right{margin-right:0}.navbar-left{float:left!important}.navbar-right{float:right!important}}.dropdown-menu{min-width:100px!important}.nav-sidebar li.active a{background:#DDD}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background:#1d9d74;color:#fff}.broadcast-message,.broadcast-message-preview{padding:10px;text-align:center;background:#555;color:#BBB;margin-top:50px}.card{position:relative;border:1px solid #d9d9d9;color:#666;background-color:#fff;width:100%;border-radius:5px}.card .card-header,.tools-header{border-top-left-radius:4px;border-top-right-radius:4px}.card .card-header{padding:9px 0;height:40px;background:#555;color:#fff;text-align:center}.card .card-body{padding:12px 10px}.card .card-footer{height:20px;font-size:10px;color:#777;margin:-15px 20px 5px}.card .detail-brand{float:left;width:30%;font-size:30px;color:#fff}.card .default{background:#1d9d74}.card .info{background:#6EBEE7}.card .warn{background:#ED7F54}.card .danger{background:#6583BE}.card .detail .text-default{color:#1d9d74}.card .detail .text-info{color:#6EBEE7}.card .detail .text-warn{color:#ED7F54}.card .detail .text-danger{color:#6583BE}.card .detail{float:right;width:70%}.card .detail .text{font-size:12px}.card .detail .number{font-size:30px;font-weight:500}.h100{height:100px}.inline{display:inline}.separator{height:1px;background-color:#e5e5e5;margin-top:10px}.card>.card-body>table>tbody>tr>td,.card>.card-body>table>thead>tr>td{word-wrap:break-word;word-break:break-all}.card>.card-body>table>thead>tr>td{font-weight:500;font-size:13px;text-align:center}.card>.card-body>table>thead>tr>td>span{font-weight:500;font-size:10px}.card>.card-body>table>tbody>tr>td{font-size:12px;text-align:center}.card>.card-body>table>tbody>tr>td>a{color:#666}.thumbnails>.card>.card-body>table>tbody>tr>td,.thumbnails>.card>.card-body>table>thead>tr>td{font-size:12px;color:#777;word-wrap:break-word;word-break:break-all}.thumbnails>.card>.card-body>table>thead>tr>td:nth-child(n+2){text-align:center}.thumbnails>.card>.card-body>table>tbody>tr>td:nth-child(n+2){font-weight:700;text-align:center}.thumbnails>.card>.card-body>table>tbody>tr>td:nth-child(1),.thumbnails>.card>.card-body>table>thead>tr>td:nth-child(1){text-align:left}.tools-header{background:#f5f5f5;padding:9px 0;height:40px}.tools-header .brand{font-size:13px;margin:2px 10px;font-weight:700;float:left}.tools-header .brand>a{color:#666}.tools-header>a,.tools-header>button,.tools-header>select{float:right;max-width:80px;margin:1px 10px;height:25px;padding:0 10px;line-height:25px;color:#666}.tools-header .paged{margin-right:0}.btn.btn-danger-tag{color:#fff;background-color:#d9534f;border-color:#d43f3a;line-height:1px;font-size:11px;padding:4px}.btn.btn-danger{color:#333;background-color:#fff;border-color:#ccc}.btn.btn-danger:active,.btn.btn-danger:focus,.btn.btn-danger:hover{color:#d9534f;border-color:#d9534f;background:#fff}.form-control{height:32px}.input-label:before{display:inline-block;content:"*";color:#f44336;font-family:SimSun;font-size:12px;-webkit-transform:TranslateX(-10px);-ms-transform:TranslateX(-10px);transform:TranslateX(-10px)}.badge-main,.label.label-main{color:#fff;background-color:#1d9d74;border-color:#1d9d74}.bootstrap-tagsinput{background-color:#fff;border:1px solid #ccc;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);display:inline-block;padding:4px 6px;color:#555;vertical-align:middle;border-radius:4px;width:85%;height:100px;line-height:20px;cursor:text}.bootstrap-tagsinput>.dropdown-menu{min-width:40px;font-size:12px}.bootstrap-tagsinput>.dropdown-menu>.active>a,.bootstrap-tagsinput>.dropdown-menu>.active>a:focus,.bootstrap-tagsinput>.dropdown-menu>.active>a:hover{background-image:-webkit-linear-gradient(top,#1d9d74 0,#1d9d74 100%);background-image:-o-linear-gradient(top,#1d9d74 0,#1d9d74 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#1d9d74),to(#1d9d74));background-image:linear-gradient(to bottom,#1d9d74 0,#1d9d74 100%);filter:progid: DXImageTransform.Microsoft.gradient(startColorstr='#1d9d74', endColorstr='#1d9d74', GradientType=0);background-repeat:repeat-x;color:#fff;text-decoration:none;outline:0;background-color:#1d9d74}.inputs-header{padding:9px 0;height:50px;border-top-left-radius:4px;border-top-right-radius:4px}.inputs-header .brand{font-size:13px;margin:2px 10px;font-weight:700;float:left}.inputs-header .brand>a{color:#666}.inputs-header>input{float:right;margin:1px 10px;height:30px;padding:0 10px;color:#666}.inputs-header>a{float:right;margin:1px 10px;height:30px;padding:5 5px}.inputs-header>select{float:right;max-width:80px;margin:1px 10px;padding:0 10px;color:#666;height:25px;font-size:12px}.witdh-150{max-width:150px}.witdh-200{max-width:200px}.card.highlight{border-color:#d9534f}.card .pagination-footer{height:40px;font-size:10px;color:#777;margin:-15px 20px 5px}.card .pagination-footer .tools{font-size:12px;margin:11px 20px 11px 0;float:right;display:inline}.card>.pagination-footer>.tools>span>input{height:25px;max-width:50px;display:inline}.pagination{display:inline-block;padding-left:0;margin:8px 0;float:right;border-radius:4px}.pagination>a{margin-right:5px;height:28px;width:28px;padding:5px 0}.datepicker>.table>tbody>tr>td,.datepicker>.table>thead>tr>td,.timepicker>.table>tbody>tr>td,.timepicker>.table>thead>tr>td{padding:5px 3px}.datepicker>.table>tbody>tr>td>.btn,.datepicker>.table>thead>tr>td>.btn,.timepicker>.table>tbody>tr>td>.btn,.timepicker>.table>thead>tr>td>.btn{border:1px solid #FFFDFD}.datepicker>.table>tbody>tr>td>.btn-default:active,.datepicker>.table>tbody>tr>td>.btn-default:focus,.datepicker>.table>tbody>tr>td>.btn-default:hover,.datepicker>.table>thead>tr>td>.btn-default:active,.datepicker>.table>thead>tr>td>.btn-default:focus,.datepicker>.table>thead>tr>td>.btn-default:hover,.timepicker>.table>tbody>tr>td>.btn-default:active,.timepicker>.table>tbody>tr>td>.btn-default:focus,.timepicker>.table>tbody>tr>td>.btn-default:hover,.timepicker>.table>thead>tr>td>.btn-default:active,.timepicker>.table>thead>tr>td>.btn-default:focus,.timepicker>.table>thead>tr>td>.btn-default:hover{color:#1d9d74;border-color:#1d9d74;background:#fff}.datepicker>.table>tbody>tr>td>a,.datepicker>.table>thead>tr>td>a,.timepicker>.table>tbody>tr>td>a,.timepicker>.table>thead>tr>td>a{height:25px;width:25px;padding:3px 0}.datepicker>.table>tbody>tr:first-child>td>a{padding:4px 0}.datepicker>.table>tbody>tr>td>a.btn.active,.datepicker>.table>thead>tr>td>a.btn.active,.timepicker>.table>tbody>tr>td>a.btn.active,.timepicker>.table>thead>tr>td>a.btn.active{color:#1d9d74;border-color:#1d9d74;background:#fff;box-shadow:inset 0 0 0 rgba(0,0,0,.125)}.datepicker>.table>thead>tr>td:not(:first-child):last-child>a,.timepicker>.table>thead>tr>td:not(:first-child):last-child>a{height:25px;width:50px;padding:5px 0}.datepicker>.table>tbody>tr>td>a,.timepicker>.table>tbody>tr>td>a{margin-left:8px}.sortorder:after{content:'\25b2'}.sortorder.reverse:after{content:'\25bc'}.input-control select{-moz-appearance:none;-webkit-appearance:none;appearance:none;position:relative;border:1px solid #d9d9d9;width:100%;height:100%;padding:.3125rem;z-index:0}.navbar-inverse{background-color:#337ab7;border-color:#337ab7}.sidebar{z-index:1;width:220px;top:0;left:0;height:100%}#page-wrapper{position:inherit;margin:70px 0 0 220px;padding:12px 30px;border-left:0 solid #e7e7e7}.sidebar .sidebar-nav.navbar-collapse{background-color:#F5F5F5;position:relative;color:#000;width:100%;padding:0;margin:0;list-style:none inside}.sidebar a{color:#555}.sidebar ul li:hover{color:red}.form-control{border-radius:8px}.form-control:focus,.highlight-border{border-color:#337ab7;box-shadow:0 0 0 rgba(0,0,0,.075) inset,0 0 0 rgba(29,157,116,1)}.browsehappy{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.btn.btn-main{color:#fff;background-color:#337ab7;border-color:#337ab7}.btn-default-inverse,.btn-default-inverse:focus,.btn-default-inverse:hover,.btn-default:active{color:#337ab7;border-color:#337ab7;background:#fff}.btn-danger-inverse,.btn-danger-inverse:focus,.btn-danger-inverse:hover,.btn-danger:active{color:#d9534f;border-color:#d9534f;background:#fff}.btn-tab-active,.btn-tab-active:focus,.btn-tab-active:hover,.btn-tab-default:active,.btn-tab-default:focus,.btn-tab-default:hover{color:#337ab7;border-color:#337ab7;background:#fff;font-weight:600}.btn-tab-default{color:#777;background:#fff;font-weight:600}.pagination>.btn.active{color:#fff;background-color:#337ab7;border-color:#337ab7}.btn-default:active,.btn-default:focus,.btn-default:hover{color:#337ab7;border-color:#337ab7;background:#fff}.bootstrap-switch.bootstrap-switch-on{border-color:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{color:#fff;background:#337ab7}.selectize-input-200>.selectize-input{min-width:200px;border-color:#337ab7} \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js new file mode 100755 index 00000000..fd191db3 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js @@ -0,0 +1 @@ +"use strict";var app;angular.module("sentinelDashboardApp",["oc.lazyLoad","ui.router","ui.bootstrap","angular-loading-bar","ngDialog","ui.bootstrap.datetimepicker","ui-notification","rzTable","angular-clipboard","selectize","angularUtils.directives.dirPagination"]).config(["$stateProvider","$urlRouterProvider","$ocLazyLoadProvider",function(e,r,a){a.config({debug:!1,events:!0}),r.otherwise("/dashboard/home"),e.state("dashboard",{url:"/dashboard",templateUrl:"app/views/dashboard/main.html",resolve:{loadMyDirectives:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/directives/header/header.js","app/scripts/directives/sidebar/sidebar.js","app/scripts/directives/sidebar/sidebar-search/sidebar-search.js"]})}]}}).state("dashboard.home",{url:"/home",templateUrl:"app/views/dashboard/home.html",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/main.js"]})}]}}).state("dashboard.flow",{templateUrl:"app/views/flow.html",url:"/flow/:app",controller:"FlowCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow.js"]})}]}}).state("dashboard.degrade",{templateUrl:"app/views/degrade.html",url:"/degrade/:app",controller:"DegradeCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/degrade.js"]})}]}}).state("dashboard.system",{templateUrl:"app/views/system.html",url:"/system/:app",controller:"SystemCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/system.js"]})}]}}).state("dashboard.machine",{templateUrl:"app/views/machine.html",url:"/app/:app",controller:"MachineCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/machine.js"]})}]}}).state("dashboard.identity",{templateUrl:"app/views/identity.html",url:"/identity/:app",controller:"IdentityCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/identity.js"]})}]}}).state("dashboard.metric",{templateUrl:"app/views/metric.html",url:"/metric/:app",controller:"MetricCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/metric.js"]})}]}})}]),(app=angular.module("sentinelDashboardApp")).filter("range",[function(){return function(e,r){if(isNaN(r)||r<=0)return[];e=[];for(var a=1;a<=r;a++)e.push(a);return e}}]),(app=angular.module("sentinelDashboardApp")).service("AppService",["$http",function(e){this.getApps=function(){return e({url:"app/briefinfos.json",method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("FlowService",["$http",function(t){this.queryMachineRules=function(e,r,a){return t({url:"flow/rules.json",params:{app:e,ip:r,port:a},method:"GET"})},this.newRule=function(e){var r={resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,strategy:e.strategy,refResource:e.refResource,controlBehavior:e.controlBehavior,warmUpPeriodSec:e.warmUpPeriodSec,maxQueueingTimeMs:e.maxQueueingTimeMs,app:e.app,ip:e.ip,port:e.port};return t({url:"/flow/new.json",params:r,method:"GET"})},this.saveRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,strategy:e.strategy,refResource:e.refResource,controlBehavior:e.controlBehavior,warmUpPeriodSec:e.warmUpPeriodSec,maxQueueingTimeMs:e.maxQueueingTimeMs};return t({url:"/flow/save.json",params:r,method:"GET"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return t({url:"/flow/delete.json",params:r,method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("DegradeService",["$http",function(t){this.queryMachineRules=function(e,r,a){return t({url:"degrade/rules.json",params:{app:e,ip:r,port:a},method:"GET"})},this.newRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,count:e.count,timeWindow:e.timeWindow,grade:e.grade,app:e.app,ip:e.ip,port:e.port};return t({url:"/degrade/new.json",params:r,method:"GET"})},this.saveRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,timeWindow:e.timeWindow};return t({url:"/degrade/save.json",params:r,method:"GET"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return t({url:"/degrade/delete.json",params:r,method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("SystemService",["$http",function(t){this.queryMachineRules=function(e,r,a){return t({url:"system/rules.json",params:{app:e,ip:r,port:a},method:"GET"})},this.newRule=function(e){var r={app:e.app,ip:e.ip,port:e.port};return 0===e.grade?r.avgLoad=e.avgLoad:1===e.grade?r.avgRt=e.avgRt:2===e.grade?r.maxThread=e.maxThread:3===e.grade&&(r.qps=e.qps),t({url:"/system/new.json",params:r,method:"GET"})},this.saveRule=function(e){var r={id:e.id};return 0===e.grade?r.avgLoad=e.avgLoad:1===e.grade?r.avgRt=e.avgRt:2===e.grade?r.maxThread=e.maxThread:3===e.grade&&(r.qps=e.qps),t({url:"/system/save.json",params:r,method:"GET"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return t({url:"/system/delete.json",params:r,method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("MachineService",["$http",function(r){this.getAppMachines=function(e){return r({url:"app/"+e+"/machines.json",method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("IdentityService",["$http",function(t){this.fetchIdentityOfMachine=function(e,r,a){return t({url:"resource/machineResource.json",params:{ip:e,port:r,searchKey:a},method:"GET"})},this.fetchClusterNodeOfMachine=function(e,r,a){return t({url:"resource/machineResource.json",params:{ip:e,port:r,type:"cluster",searchKey:a},method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("MetricService",["$http",function(s){this.queryAppSortedIdentities=function(e){return s({url:"/metric/queryTopResourceMetric.json",params:e,method:"GET"})},this.queryByAppAndIdentity=function(e){return s({url:"/metric/queryByAppAndResource.json",params:e,method:"GET"})},this.queryByMachineAndIdentity=function(e,r,a,t,o){var i={ip:e,port:r,identity:a,startTime:t.getTime(),endTime:o.getTime()};return s({url:"/metric/queryByAppAndResource.json",params:i,method:"GET"})}}]); \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/dist/js/app.vendor.js b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.vendor.js new file mode 100755 index 00000000..52b9b985 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.vendor.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("angular")):"function"==typeof define&&define.amd?define(["exports","angular"],t):t(e["@uirouter/angularjs"]={},e.angular)}(this,function(g,e){"use strict";var t=angular,C=e&&e.module?e:t;function u(n){var e=[].slice.apply(arguments,[1]),r=n.length;return function e(t){return t.length>=r?n.apply(null,t):function(){return e(t.concat([].slice.apply(arguments)))}}(e)}function n(){var n=arguments,r=n.length-1;return function(){for(var e=r,t=n[r].apply(this,arguments);e--;)t=n[e].call(this,t);return t}}function l(){for(var e=[],t=0;tthis._limit&&this.evict(),e},e.prototype.evict=function(){var t=this._items.shift();return this._evictListeners.forEach(function(e){return e(t)}),t},e.prototype.dequeue=function(){if(this.size())return this._items.splice(0,1)[0]},e.prototype.clear=function(){var e=this._items;return this._items=[],e},e.prototype.size=function(){return this._items.length},e.prototype.remove=function(e){var t=this._items.indexOf(e);return-1 "+Ue(e))},e.prototype.traceTransitionIgnored=function(e){this.enabled(g.Category.TRANSITION)&&console.log(st(e)+": Ignored <> "+Ue(e))},e.prototype.traceHookInvocation=function(e,t,n){if(this.enabled(g.Category.HOOK)){var r=S("traceData.hookType")(n)||"internal",i=S("traceData.context.state.name")(n)||S("traceData.context")(n)||"unknown",o=He(e.registeredHook.callback);console.log(st(t)+": Hook -> "+r+" context: "+i+", "+Fe(200,o))}},e.prototype.traceHookResult=function(e,t,n){this.enabled(g.Category.HOOK)&&console.log(st(t)+": <- Hook returned: "+Fe(200,Ue(e)))},e.prototype.traceResolvePath=function(e,t,n){this.enabled(g.Category.RESOLVE)&&console.log(st(n)+": Resolving "+e+" ("+t+")")},e.prototype.traceResolvableResolved=function(e,t){this.enabled(g.Category.RESOLVE)&&console.log(st(t)+": <- Resolved "+e+" to: "+Fe(200,Ue(e.data)))},e.prototype.traceError=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Rejected "+Ue(t)+", reason: "+e)},e.prototype.traceSuccess=function(e,t){this.enabled(g.Category.TRANSITION)&&console.log(st(t)+": <- Success "+Ue(t)+", final state: "+e.name)},e.prototype.traceUIViewEvent=function(e,t,n){void 0===n&&(n=""),this.enabled(g.Category.UIVIEW)&&console.log("ui-view: "+Le(30,e)+" "+et(t)+n)},e.prototype.traceUIViewConfigUpdated=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Updating",e," with ViewConfig from context='"+t+"'")},e.prototype.traceUIViewFill=function(e,t){this.enabled(g.Category.UIVIEW)&&this.traceUIViewEvent("Fill",e," with: "+Fe(200,t))},e.prototype.traceViewSync=function(e){if(this.enabled(g.Category.VIEWCONFIG)){var a="uiview component fqn",t=e.map(function(e){var t,n=e.uiView,r=e.viewConfig,i=n&&n.fqn,o=r&&r.viewDecl.$context.name+": ("+r.viewDecl.$name+")";return(t={})[a]=i,t["view config state (view name)"]=o,t}).sort(function(e,t){return(e[a]||"").localeCompare(t[a]||"")});it(t)}},e.prototype.traceViewServiceEvent=function(e,t){var n,r,i;this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+(r=(n=t).viewDecl,i=r.$context.name||"(root)","[View#"+n.$id+" from '"+i+"' state]: target ui-view: '"+r.$uiViewName+"@"+r.$uiViewContextAnchor+"'"))},e.prototype.traceViewServiceUIViewEvent=function(e,t){this.enabled(g.Category.VIEWCONFIG)&&console.log("VIEWCONFIG: "+e+" "+et(t))},e}(),ut=new lt,ct=function(){function e(e){this.pattern=/.*/,this.inherit=!0,N(this,e)}return e.prototype.is=function(e,t){return!0},e.prototype.encode=function(e,t){return e},e.prototype.decode=function(e,t){return e},e.prototype.equals=function(e,t){return e==t},e.prototype.$subPattern=function(){var e=this.pattern.toString();return e.substr(1,e.length-2)},e.prototype.toString=function(){return"{ParamType:"+this.name+"}"},e.prototype.$normalize=function(e){return this.is(e)?e:this.decode(e)},e.prototype.$asArray=function(e,t){if(!e)return this;if("auto"===e&&!t)throw new Error("'auto' array mode is for query parameters only");return new dt(this,e)},e}();function dt(r,i){var o=this;function a(e){return E(e)?e:k(e)?[e]:[]}function s(n,r){return function(e){if(E(e)&&0===e.length)return e;var t=ce(a(e),n);return!0===r?0===se(t,function(e){return!e}).length:function(e){switch(e.length){case 0:return;case 1:return"auto"===i?e[0]:e;default:return e}}(t)}}function l(o){return function(e,t){var n=a(e),r=a(t);if(n.length!==r.length)return!1;for(var i=0;i=n.invokeLimit&&n.deregister()}}},o.prototype.handleHookResult=function(e){var t=this,n=this.getNotCurrentRejection();return n||(R(e)?e.then(function(e){return t.handleHookResult(e)}):(ut.traceHookResult(e,this.transition,this.options),!1===e?Ve.aborted("Hook aborted transition").toPromise():h($t)(e)?Ve.redirected(e).toPromise():void 0))},o.prototype.getNotCurrentRejection=function(){var e=this.transition.router;return e._disposed?Ve.aborted("UIRouter instance #"+e.$id+" has been stopped (disposed)").toPromise():this.transition._aborted?Ve.aborted().toPromise():this.isSuperseded()?Ve.superseded(this.options.current()).toPromise():void 0},o.prototype.toString=function(){var e=this.options,t=this.registeredHook;return(S("traceData.hookType")(e)||"internal")+" context: "+(S("traceData.context.state.name")(e)||S("traceData.context")(e)||"unknown")+", "+Fe(200,Ye(t.callback))},o.HANDLE_RESULT=function(t){return function(e){return t.handleHookResult(e)}},o.LOG_REJECTED_RESULT=function(t){return function(e){R(e)&&e.catch(function(e){return t.logError(Ve.normalize(e))})}},o.LOG_ERROR=function(t){return function(e){return t.logError(e)}},o.REJECT_ERROR=function(e){return function(e){return Pe(e)}},o.THROW_ERROR=function(e){return function(e){throw e}},o}();function Gt(e,t){var i=O(t)?[t]:t;return!!(D(i)?i:function(e){for(var t=i,n=0;n "+(this.valid()?"":"(X) ")+"'"+(T(t)?t.name:t)+"'"+Ue(n(this.params()))+" )"},t.diToken=t}();function en(e,t){var n=["",""],r=e.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!t)return r;switch(t.squash){case!1:n=["(",")"+(t.isOptional?"?":"")];break;case!0:r=r.replace(/\/$/,""),n=["(?:/(",")|/)?"];break;default:n=["("+t.squash+"|",")?"]}return r+n[0]+t.type.pattern.source+n[1]}var tn=Xe("/"),nn={state:{params:{}},strict:!0,caseInsensitive:!0},rn=function(){function m(o,a,e,t){var s=this;this._cache={path:[this]},this._children=[],this._params=[],this._segments=[],this._compiled=[],this.config=t=te(t,nn),this.pattern=o;for(var n,r,i,l=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,u=/([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,c=[],d=0,p=function(e){if(!m.nameValidator.test(e))throw new Error("Invalid parameter name '"+e+"' in pattern '"+o+"'");if(le(s._params,v("id",e)))throw new Error("Duplicate parameter name '"+e+"' in pattern '"+o+"'")},h=function(e,t){var n,r=e[2]||e[3],i=t?e[4]:e[4]||("*"===e[1]?"[\\s\\S]*":null);return{id:r,regexp:i,segment:o.substring(d,e.index),type:i?a.type(i)||(n=i,W(a.type(t?"query":"path"),{pattern:new RegExp(n,s.config.caseInsensitive?"i":void 0)})):null}};(n=l.exec(o))&&!(0<=(r=h(n,!1)).segment.indexOf("?"));)p(r.id),this._params.push(e.fromPath(r.id,r.type,t.state)),this._segments.push(r.segment),c.push([r.segment,De(this._params)]),d=l.lastIndex;var f=(i=o.substring(d)).indexOf("?");if(0<=f){var g=i.substring(f);if(i=i.substring(0,f),0r.weight?s:r}return r},t.prototype.sync=function(e){if(!e||!e.defaultPrevented){var t=this._router,n=t.urlService,r=t.stateService,i={path:n.path(),search:n.search(),hash:n.hash()},o=this.match(i);m([[O,function(e){return n.url(e,!0)}],[$t.isDef,function(e){return r.go(e.state,e.params,e.options)}],[h($t),function(e){return r.go(e.state(),e.params(),e.options())}]])(o&&o.rule.handler(o.match,i,t))}},t.prototype.listen=function(e){var t=this;if(!1!==e)return this._stopFn=this._stopFn||this._router.urlService.onChange(function(e){return t.sync(e)});this._stopFn&&this._stopFn(),delete this._stopFn},t.prototype.update=function(e){var t=this._router.locationService;e?this.location=t.url():t.url()!==this.location&&t.url(this.location,!0)},t.prototype.push=function(e,t,n){var r=n&&!!n.replace;this._router.urlService.url(e.format(t||{}),r)},t.prototype.href=function(e,t,n){var r=e.format(t);if(null==r)return null;n=n||{absolute:!1};var i,o,a,s,l=this._router.urlService.config,u=l.html5Mode();if(u||null===r||(r="#"+l.hashPrefix()+r),i=r,o=u,a=n.absolute,r="/"===(s=l.baseHref())?i:o?We(s)+i:a?s.slice(1)+i:i,!n.absolute||!r)return r;var c=!u&&r?"/":"",d=l.port(),p=80===d||443===d?"":":"+d;return[l.protocol(),"://",l.host(),p,c,r].join("")},t.prototype.rule=function(e){var t=this;if(!ln.isUrlRule(e))throw new Error("invalid rule");return e.$id=this._id++,e.priority=e.priority||0,this._rules.push(e),this._sorted=!1,function(){return t.removeRule(e)}},t.prototype.removeRule=function(e){Q(this._rules,e)},t.prototype.rules=function(){return this.ensureSorted(),this._rules.slice()},t.prototype.otherwise=function(e){var t=pn(e);this._otherwiseFn=this.urlRuleFactory.create(f(!0),t),this._sorted=!1},t.prototype.initial=function(e){var t=pn(e);this.rule(this.urlRuleFactory.create(function(e,t){return 0===t.globals.transitionHistory.size()&&!!/^\/?$/.exec(e.path)},t))},t.prototype.when=function(e,t,n){var r=this.urlRuleFactory.create(e,t);return k(n&&n.priority)&&(r.priority=n.priority),this.rule(r),r},t.prototype.deferIntercept=function(e){void 0===e&&(e=!0),this.interceptDeferred=e},t}();function pn(e){if(!(D(e)||O(e)||h($t)(e)||$t.isDef(e)))throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");return D(e)?e:f(e)}var hn=function(){function l(e){var n=this;this.router=e,this._uiViews=[],this._viewConfigs=[],this._viewConfigFactories={},this._listeners=[],this._pluginapi={_rootViewContext:this._rootViewContext.bind(this),_viewConfigFactory:this._viewConfigFactory.bind(this),_registeredUIView:function(t){return le(n._uiViews,function(e){return n.router.$id+"."+e.id===t})},_registeredUIViews:function(){return n._uiViews},_activeViewConfigs:function(){return n._viewConfigs},_onSync:function(e){return n._listeners.push(e),function(){return Q(n._listeners,e)}}}}return l.normalizeUIViewTarget=function(e,t){void 0===t&&(t="");var n=t.split("@"),r=n[0]||"$default",i=O(n[1])?n[1]:"^",o=/^(\^(?:\.\^)*)\.(.*$)/.exec(r);o&&(i=o[1],r=o[2]),"!"===r.charAt(0)&&(r=r.substr(1),i="");/^(\^(?:\.\^)*)$/.exec(i)?i=i.split(".").reduce(function(e,t){return e.parent},e).name:"."===i&&(i=e.name);return{uiViewName:r,uiViewContextAnchor:i}},l.prototype._rootViewContext=function(e){return this._rootContext=e||this._rootContext},l.prototype._viewConfigFactory=function(e,t){this._viewConfigFactories[e]=t},l.prototype.createViewConfig=function(e,t){var n=this._viewConfigFactories[t.$type];if(!n)throw new Error("ViewService: No view config factory registered for type "+t.$type);var r=n(e,t);return E(r)?r:[r]},l.prototype.deactivateViewConfig=function(e){ut.traceViewServiceEvent("<- Removing",e),Q(this._viewConfigs,e)},l.prototype.activateViewConfig=function(e){ut.traceViewServiceEvent("-> Registering",e),this._viewConfigs.push(e)},l.prototype.sync=function(){var n=this,r=this._uiViews.map(function(e){return[e.fqn,e]}).reduce(ke,{});function i(e){for(var t=e.viewDecl.$context,n=0;++n&&t.parent;)t=t.parent;return n}var o=u(function(e,t,n,r){return t*(e(n)-e(r))}),e=this._uiViews.sort(o(function(e){var t=function(e){return e&&e.parent?t(e.parent)+1:1};return 1e4*e.fqn.split(".").length+t(e.creationContext)},1)).map(function(e){var t=n._viewConfigs.filter(l.matches(r,e));return 1 Registering",t);var e=this._uiViews;return e.filter(function(e){return e.fqn===t.fqn&&e.$type===t.$type}).length&&ut.traceViewServiceUIViewEvent("!!!! duplicate uiView named:",t),e.push(t),this.sync(),function(){-1!==e.indexOf(t)?(ut.traceViewServiceUIViewEvent("<- Deregistering",t),Q(e)(t)):ut.traceViewServiceUIViewEvent("Tried removing non-registered uiView",t)}},l.prototype.available=function(){return this._uiViews.map(w("fqn"))},l.prototype.active=function(){return this._uiViews.filter(w("$config")).map(w("name"))},l.matches=function(s,l){return function(e){if(l.$type!==e.viewDecl.$type)return!1;var t=e.viewDecl,n=t.$uiViewName.split("."),r=l.fqn.split(".");if(!q(n,r.slice(0-n.length)))return!1;var i=1-n.length||void 0,o=r.slice(0,i).join("."),a=s[o].creationContext;return t.$uiViewContextAnchor===(a&&a.name)}},l}(),fn=function(){function e(){this.params=new wt,this.lastStartedTransitionId=-1,this.transitionHistory=new Re([],1),this.successfulTransitions=new Re([],1)}return e.prototype.dispose=function(){this.transitionHistory.clear(),this.successfulTransitions.clear(),this.transition=null},e}(),gn=function(e){return e.reduce(function(e,t){return e[t]=I(t),e},{dispose:z})},mn=["url","path","search","hash","onChange"],vn=["port","protocol","host","baseHref","html5Mode","hashPrefix"],yn=["type","caseInsensitive","strictMode","defaultSquashPolicy"],wn=["sort","when","initial","otherwise","rules","rule","removeRule"],bn=["deferIntercept","listen","sync","match"],$n=function(){function e(e,t){void 0===t&&(t=!0),this.router=e,this.rules={},this.config={};var n=function(){return e.locationService};B(n,this,n,mn,t);var r=function(){return e.locationConfig};B(r,this.config,r,vn,t);var i=function(){return e.urlMatcherFactory};B(i,this.config,i,yn);var o=function(){return e.urlRouter};B(o,this.rules,o,wn),B(o,this,o,bn)}return e.prototype.url=function(e,t,n){},e.prototype.path=function(){},e.prototype.search=function(){},e.prototype.hash=function(){},e.prototype.onChange=function(e){},e.prototype.parts=function(){return{path:this.path(),search:this.search(),hash:this.hash()}},e.prototype.dispose=function(){},e.prototype.sync=function(e){},e.prototype.listen=function(e){},e.prototype.deferIntercept=function(e){},e.prototype.match=function(e){},e.locationServiceStub=gn(mn),e.locationConfigStub=gn(vn),e}(),_n=0,Cn=function(){function e(e,t){void 0===e&&(e=$n.locationServiceStub),void 0===t&&(t=$n.locationConfigStub),this.locationService=e,this.locationConfig=t,this.$id=_n++,this._disposed=!1,this._disposables=[],this.trace=ut,this.viewService=new hn(this),this.globals=new fn,this.transitionService=new zn(this),this.urlMatcherFactory=new sn,this.urlRouter=new dn(this),this.stateRegistry=new zt(this),this.stateService=new Bn(this),this.urlService=new $n(this),this._plugins={},this.viewService._pluginapi._rootViewContext(this.stateRegistry.root()),this.globals.$current=this.stateRegistry.root(),this.globals.current=this.globals.$current.self,this.disposable(this.globals),this.disposable(this.stateService),this.disposable(this.stateRegistry),this.disposable(this.transitionService),this.disposable(this.urlRouter),this.disposable(e),this.disposable(t)}return e.prototype.disposable=function(e){this._disposables.push(e)},e.prototype.dispose=function(e){var t=this;e&&D(e.dispose)?e.dispose(this):(this._disposed=!0,this._disposables.slice().forEach(function(e){try{"function"==typeof e.dispose&&e.dispose(t),Q(t._disposables,e)}catch(e){}}))},e.prototype.plugin=function(e,t){void 0===t&&(t={});var n=new e(this,t);if(!n.name)throw new Error("Required property `name` missing on plugin: "+n);return this._disposables.push(n),this._plugins[n.name]=n},e.prototype.getPlugin=function(e){return e?this._plugins[e]:de(this._plugins)},e}();function Sn(t){t.addResolvable(kt.fromData(Cn,t.router),""),t.addResolvable(kt.fromData(Jt,t),""),t.addResolvable(kt.fromData("$transition$",t),""),t.addResolvable(kt.fromData("$stateParams",t.params()),""),t.entering().forEach(function(e){t.addResolvable(kt.fromData("$state$",e),e)})}var kn=G(["$transition$",Jt]),Dn=function(e){var t=de(e.treeChanges()).reduce(fe,[]).reduce(ve,[]),n=function(e){return kn(e.token)?kt.fromData(e.token,null):e};t.forEach(function(e){e.resolvables=e.resolvables.map(n)})},xn=function(t){var e=t.to().redirectTo;if(e){var n=t.router.stateService;return D(e)?V.$q.when(e(t)).then(r):r(e)}function r(e){if(e)return e instanceof $t?e:O(e)?n.target(e,t.params(),t.options()):e.state||e.params?n.target(e.state||t.to(),e.params||t.params(),t.options()):void 0}};function On(n){return function(e,t){return(0,t.$$state()[n])(e,t)}}var Tn=On("onExit"),En=On("onRetain"),An=On("onEnter"),Pn=function(e){return new Et(e.treeChanges().to).resolvePath("EAGER",e).then(z)},Mn=function(e,t){return new Et(e.treeChanges().to).subContext(t.$$state()).resolvePath("LAZY",e).then(z)},Rn=function(e){return new Et(e.treeChanges().to).resolvePath("LAZY",e).then(z)},In=function(e){var t=V.$q,n=e.views("entering");if(n.length)return t.all(n.map(function(e){return t.when(e.load())})).then(z)},Vn=function(e){var t=e.views("entering"),n=e.views("exiting");if(t.length||n.length){var r=e.router.viewService;n.forEach(function(e){return r.deactivateViewConfig(e)}),t.forEach(function(e){return r.activateViewConfig(e)}),r.sync()}},Fn=function(e){var t=e.router.globals,n=function(){t.transition===e&&(t.transition=null)};e.onSuccess({},function(){t.successfulTransitions.enqueue(e),t.$current=e.$to(),t.current=t.$current.self,xe(e.params(),t.params)},{priority:1e4}),e.promise.then(n,n)},Ln=function(e){var t=e.options(),n=e.router.stateService,r=e.router.urlRouter;if("url"!==t.source&&t.location&&n.$current.navigable){var i={replace:"replace"===t.location};r.push(n.$current.navigable.url,n.params,i)}r.update(!0)},jn=function(a){var s=a.router;var e=a.entering().filter(function(e){return!!e.$$state().lazyLoad}).map(function(e){return Hn(a,e)});return V.$q.all(e).then(function(){if("url"!==a.originalTransition().options().source){var e=a.targetState();return s.stateService.target(e.identifier(),e.params(),e.options())}var t=s.urlService,n=t.match(t.parts()),r=n&&n.rule;if(r&&"STATE"===r.type){var i=r.state,o=n.match;return s.stateService.target(i,o,a.options())}s.urlService.sync()})};function Hn(t,n){var r=n.$$state().lazyLoad,e=r._promise;if(!e){e=r._promise=V.$q.when(r(t,n)).then(function(e){e&&Array.isArray(e.states)&&e.states.forEach(function(e){return t.router.stateRegistry.register(e)});return e}).then(function(e){return delete n.lazyLoad,delete n.$$state().lazyLoad,delete r._promise,e},function(e){return delete r._promise,V.$q.reject(e)})}return e}var Yn=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1),this.name=e,this.hookPhase=t,this.hookOrder=n,this.criteriaMatchPath=r,this.reverseSort=i,this.getResultHandler=o,this.getErrorHandler=a,this.synchronous=s};function Nn(e){var t=e._ignoredReason();if(t){ut.traceTransitionIgnored(e);var n=e.router.globals.transition;return"SameAsCurrent"===t&&n&&n.abort(),Ve.ignored().toPromise()}}function qn(e){if(!e.valid())throw new Error(e.error().toString())}var Un={location:!0,relative:null,inherit:!1,notify:!0,reload:!1,custom:{},current:function(){return null},source:"unknown"},zn=function(){function e(e){this._transitionCount=0,this._eventTypes=[],this._registeredHooks={},this._criteriaPaths={},this._router=e,this.$view=e.viewService,this._deregisterHookFns={},this._pluginapi=B(f(this),{},f(this),["_definePathType","_defineEvent","_getPathTypes","_getEvents","getHooks"]),this._defineCorePaths(),this._defineCoreEvents(),this._registerCoreTransitionHooks(),e.globals.successfulTransitions.onEvict(Dn)}return e.prototype.onCreate=function(e,t,n){},e.prototype.onBefore=function(e,t,n){},e.prototype.onStart=function(e,t,n){},e.prototype.onExit=function(e,t,n){},e.prototype.onRetain=function(e,t,n){},e.prototype.onEnter=function(e,t,n){},e.prototype.onFinish=function(e,t,n){},e.prototype.onSuccess=function(e,t,n){},e.prototype.onError=function(e,t,n){},e.prototype.dispose=function(e){de(this._registeredHooks).forEach(function(t){return t.forEach(function(e){e._deregistered=!0,Q(t,e)})})},e.prototype.create=function(e,t){return new Jt(e,t,this._router)},e.prototype._defineCoreEvents=function(){var e=g.TransitionHookPhase,t=Wt,n=this._criteriaPaths;this._defineEvent("onCreate",e.CREATE,0,n.to,!1,t.LOG_REJECTED_RESULT,t.THROW_ERROR,!0),this._defineEvent("onBefore",e.BEFORE,0,n.to),this._defineEvent("onStart",e.RUN,0,n.to),this._defineEvent("onExit",e.RUN,100,n.exiting,!0),this._defineEvent("onRetain",e.RUN,200,n.retained),this._defineEvent("onEnter",e.RUN,300,n.entering),this._defineEvent("onFinish",e.RUN,400,n.to),this._defineEvent("onSuccess",e.SUCCESS,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0),this._defineEvent("onError",e.ERROR,0,n.to,!1,t.LOG_REJECTED_RESULT,t.LOG_ERROR,!0)},e.prototype._defineCorePaths=function(){var e=g.TransitionHookScope.STATE,t=g.TransitionHookScope.TRANSITION;this._definePathType("to",t),this._definePathType("from",t),this._definePathType("exiting",e),this._definePathType("retained",e),this._definePathType("entering",e)},e.prototype._defineEvent=function(e,t,n,r,i,o,a,s){void 0===i&&(i=!1),void 0===o&&(o=Wt.HANDLE_RESULT),void 0===a&&(a=Wt.REJECT_ERROR),void 0===s&&(s=!1);var l=new Yn(e,t,n,r,i,o,a,s);this._eventTypes.push(l),Qt(this,this,l)},e.prototype._getEvents=function(t){return(k(t)?this._eventTypes.filter(function(e){return e.hookPhase===t}):this._eventTypes.slice()).sort(function(e,t){var n=e.hookPhase-t.hookPhase;return 0===n?e.hookOrder-t.hookOrder:n})},e.prototype._definePathType=function(e,t){this._criteriaPaths[e]={name:e,scope:t}},e.prototype._getPathTypes=function(){return this._criteriaPaths},e.prototype.getHooks=function(e){return this._registeredHooks[e]},e.prototype._registerCoreTransitionHooks=function(){var e=this._deregisterHookFns;e.addCoreResolves=this.onCreate({},Sn),e.ignored=this.onBefore({},Nn,{priority:-9999}),e.invalid=this.onBefore({},qn,{priority:-1e4}),e.redirectTo=this.onStart({to:function(e){return!!e.redirectTo}},xn),e.onExit=this.onExit({exiting:function(e){return!!e.onExit}},Tn),e.onRetain=this.onRetain({retained:function(e){return!!e.onRetain}},En),e.onEnter=this.onEnter({entering:function(e){return!!e.onEnter}},An),e.eagerResolve=this.onStart({},Pn,{priority:1e3}),e.lazyResolve=this.onEnter({entering:f(!0)},Mn,{priority:1e3}),e.resolveAll=this.onFinish({},Rn,{priority:1e3}),e.loadViews=this.onFinish({},In),e.activateViews=this.onSuccess({},Vn),e.updateGlobals=this.onCreate({},Fn),e.updateUrl=this.onSuccess({},Ln,{priority:9999}),e.lazyLoad=this.onBefore({entering:function(e){return!!e.lazyLoad}},jn)},e}(),Bn=function(){function n(e){this.router=e,this.invalidCallbacks=[],this._defaultErrorHandler=function(e){e instanceof Error&&e.stack?(console.error(e),console.error(e.stack)):e instanceof Ve?(console.error(e.toString()),e.detail&&e.detail.stack&&console.error(e.detail.stack)):console.error(e)};var t=Object.keys(n.prototype).filter(d(G(["current","$current","params","transition"])));B(f(n.prototype),this,f(this),t)}return Object.defineProperty(n.prototype,"transition",{get:function(){return this.router.globals.transition},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"params",{get:function(){return this.router.globals.params},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"current",{get:function(){return this.router.globals.current},enumerable:!0,configurable:!0}),Object.defineProperty(n.prototype,"$current",{get:function(){return this.router.globals.$current},enumerable:!0,configurable:!0}),n.prototype.dispose=function(){this.defaultErrorHandler(z),this.invalidCallbacks=[]},n.prototype._handleInvalidTargetState=function(e,n){var r=this,i=_t.makeTargetState(this.router.stateRegistry,e),t=this.router.globals,o=function(){return t.transitionHistory.peekTail()},a=o(),s=new Re(this.invalidCallbacks.slice()),l=new Et(e).injector(),u=function(e){if(e instanceof $t){var t=e;return(t=r.target(t.identifier(),t.params(),t.options())).valid()?o()!==a?Ve.superseded().toPromise():r.transitionTo(t.identifier(),t.params(),t.options()):Ve.invalid(t.error()).toPromise()}};return function t(){var e=s.dequeue();return void 0===e?Ve.invalid(n.error()).toPromise():V.$q.when(e(n,i,l)).then(u).then(function(e){return e||t()})}()},n.prototype.onInvalid=function(e){return this.invalidCallbacks.push(e),function(){Q(this.invalidCallbacks)(e)}.bind(this)},n.prototype.reload=function(e){return this.transitionTo(this.current,this.params,{reload:!k(e)||e,inherit:!1,notify:!1})},n.prototype.go=function(e,t,n){var r=te(n,{relative:this.$current,inherit:!0},Un);return this.transitionTo(e,t,r)},n.prototype.target=function(e,t,n){if(void 0===n&&(n={}),T(n.reload)&&!n.reload.name)throw new Error("Invalid reload state object");var r=this.router.stateRegistry;if(n.reloadState=!0===n.reload?r.root():r.matcher.find(n.reload,n.relative),n.reload&&!n.reloadState)throw new Error("No such reload state '"+(O(n.reload)?n.reload:n.reload.name)+"'");return new $t(this.router.stateRegistry,e,t,n)},n.prototype.getCurrentPath=function(){var e=this,t=this.router.globals.successfulTransitions.peekTail();return t?t.treeChanges().to:[new bt(e.router.stateRegistry.root())]},n.prototype.transitionTo=function(e,t,n){var o=this;void 0===t&&(t={}),void 0===n&&(n={});var a=this.router,s=a.globals;n=te(n,Un);n=N(n,{current:function(){return s.transition}});var r=this.target(e,t,n),i=this.getCurrentPath();if(!r.exists())return this._handleInvalidTargetState(i,r);if(!r.valid())return Pe(r.error());var l=function(i){return function(e){if(e instanceof Ve){var t=a.globals.lastStartedTransitionId===i.$id;if(e.type===g.RejectType.IGNORED)return t&&a.urlRouter.update(),V.$q.when(s.current);var n=e.detail;if(e.type===g.RejectType.SUPERSEDED&&e.redirected&&n instanceof $t){var r=i.redirect(n);return r.run().catch(l(r))}if(e.type===g.RejectType.ABORTED)return t&&a.urlRouter.update(),V.$q.reject(e)}return o.defaultErrorHandler()(e),V.$q.reject(e)}},u=this.router.transitionService.create(i,r),c=u.run().catch(l(u));return Ae(c),N(c,{transition:u})},n.prototype.is=function(e,t,n){n=te(n,{relative:this.$current});var r=this.router.stateRegistry.matcher.find(e,n.relative);if(k(r)){if(this.$current!==r)return!1;if(!t)return!0;var i=r.parameters({inherit:!0,matchingKeys:t});return vt.equals(i,vt.values(i,t),this.params)}},n.prototype.includes=function(e,t,n){n=te(n,{relative:this.$current});var r=O(e)&&Me.fromString(e);if(r){if(!r.matches(this.$current.name))return!1;e=this.$current.name}var i=this.router.stateRegistry.matcher.find(e,n.relative),o=this.$current.includes;if(k(i)){if(!k(o[i.name]))return!1;if(!t)return!0;var a=i.parameters({inherit:!0,matchingKeys:t});return vt.equals(a,vt.values(a,t),this.params)}},n.prototype.href=function(e,t,n){n=te(n,{lossy:!0,inherit:!0,absolute:!1,relative:this.$current}),t=t||{};var r=this.router.stateRegistry.matcher.find(e,n.relative);if(!k(r))return null;n.inherit&&(t=this.params.$inherit(t,this.$current,r));var i=r&&n.lossy?r.navigable:r;return i&&void 0!==i.url&&null!==i.url?this.router.urlRouter.href(i.url,t,{absolute:n.absolute}):null},n.prototype.defaultErrorHandler=function(e){return this._defaultErrorHandler=e||this._defaultErrorHandler},n.prototype.get=function(e,t){var n=this.router.stateRegistry;return 0===arguments.length?n.get():n.get(e,t||this.$current)},n.prototype.lazyLoad=function(e,t){var n=this.get(e);if(!n||!n.lazyLoad)throw new Error("Can not lazy load "+e);var r=this.getCurrentPath(),i=_t.makeTargetState(this.router.stateRegistry,r);return Hn(t=t||this.router.transitionService.create(r,i),n)},n}(),Wn={when:function(n){return new Promise(function(e,t){return e(n)})},reject:function(n){return new Promise(function(e,t){t(n)})},defer:function(){var n={};return n.promise=new Promise(function(e,t){n.resolve=e,n.reject=t}),n},all:function(e){if(E(e))return Promise.all(e);if(T(e)){var t=Object.keys(e).map(function(t){return e[t].then(function(e){return{key:t,val:e}})});return Wn.all(t).then(function(e){return e.reduce(function(e,t){return e[t.key]=t.val,e},{})})}}},Gn={},Kn=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm,Qn=/([^\s,]+)/g,Zn={get:function(e){return Gn[e]},has:function(e){return null!=Zn.get(e)},invoke:function(e,t,n){var r=N({},Gn,n||{}),i=Zn.annotate(e),o=be(function(e){return r.hasOwnProperty(e)},function(e){return"DI can't find injectable: '"+e+"'"}),a=i.filter(o).map(function(e){return r[e]});return D(e)?e.apply(t,a):e.slice(-1)[0].apply(t,a)},annotate:function(e){if(!M(e))throw new Error("Not an injectable function: "+e);if(e&&e.$inject)return e.$inject;if(E(e))return e.slice(0,-1);var t=e.toString().replace(Kn,"");return t.slice(t.indexOf("(")+1,t.indexOf(")")).match(Qn)||[]}},Xn=function(e,t){var n=t[0],r=t[1];return e.hasOwnProperty(n)?E(e[n])?e[n].push(r):e[n]=[e[n],r]:e[n]=r,e},Jn=function(e){return e.split("&").filter(U).map(Qe).reduce(Xn,{})};function er(e){var t=function(e){return e||""},n=Ge(e).map(t),r=n[0],i=n[1],o=Ke(r).map(t);return{path:o[0],search:o[1],hash:i,url:e}}var tr=function(e){var t=e.path(),n=e.search(),r=e.hash(),i=Object.keys(n).map(function(t){var e=n[t];return(E(e)?e:[e]).map(function(e){return t+"="+e})}).reduce(fe,[]).join("&");return t+(i?"?"+i:"")+(r?"#"+r:"")};function nr(r,i,o,a){return function(e){var t=e.locationService=new o(e),n=e.locationConfig=new a(e,i);return{name:r,service:t,configuration:n,dispose:function(e){e.dispose(t),e.dispose(n)}}}}var rr,ir,or,ar=function(){function e(e,t){var n=this;this.fireAfterUpdate=t,this._listeners=[],this._listener=function(t){return n._listeners.forEach(function(e){return e(t)})},this.hash=function(){return er(n._get()).hash},this.path=function(){return er(n._get()).path},this.search=function(){return Jn(er(n._get()).search)},this._location=F.location,this._history=F.history}return e.prototype.url=function(t,e){return void 0===e&&(e=!0),k(t)&&t!==this._get()&&(this._set(null,null,t,e),this.fireAfterUpdate&&this._listeners.forEach(function(e){return e({url:t})})),tr(this)},e.prototype.onChange=function(e){var t=this;return this._listeners.push(e),function(){return Q(t._listeners,e)}},e.prototype.dispose=function(e){ee(this._listeners)},e}(),sr=(rr=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}rr(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),lr=function(n){function e(e){var t=n.call(this,e,!1)||this;return F.addEventListener("hashchange",t._listener,!1),t}return sr(e,n),e.prototype._get=function(){return Ze(this._location.hash)},e.prototype._set=function(e,t,n,r){this._location.hash=n},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),F.removeEventListener("hashchange",this._listener)},e}(ar),ur=(ir=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}ir(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),cr=function(t){function e(e){return t.call(this,e,!0)||this}return ur(e,t),e.prototype._get=function(){return this._url},e.prototype._set=function(e,t,n,r){this._url=n},e}(ar),dr=(or=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}or(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),pr=function(n){function e(e){var t=n.call(this,e,!0)||this;return t._config=e.urlService.config,F.addEventListener("popstate",t._listener,!1),t}return dr(e,n),e.prototype._getBasePrefix=function(){return We(this._config.baseHref())},e.prototype._get=function(){var e=this._location,t=e.pathname,n=e.hash,r=e.search;r=Ke(r)[1],n=Ge(n)[1];var i=this._getBasePrefix(),o=t===this._config.baseHref(),a=t.substr(0,i.length)===i;return(t=o?"/":a?t.substring(i.length):t)+(r?"?"+r:"")+(n?"#"+n:"")},e.prototype._set=function(e,t,n,r){var i=this._getBasePrefix(),o=n&&"/"!==n[0]?"/":"",a=""===n||"/"===n?this._config.baseHref():i+o+n;r?this._history.replaceState(e,t,a):this._history.pushState(e,t,a)},e.prototype.dispose=function(e){n.prototype.dispose.call(this,e),F.removeEventListener("popstate",this._listener)},e}(ar),hr=function(){var t=this;this.dispose=z,this._baseHref="",this._port=80,this._protocol="http",this._host="localhost",this._hashPrefix="",this.port=function(){return t._port},this.protocol=function(){return t._protocol},this.host=function(){return t._host},this.baseHref=function(){return t._baseHref},this.html5Mode=function(){return!1},this.hashPrefix=function(e){return k(e)?t._hashPrefix=e:t._hashPrefix}},fr=function(){function e(e,t){void 0===t&&(t=!1),this._isHtml5=t,this._baseHref=void 0,this._hashPrefix=""}return e.prototype.port=function(){return location.port?Number(location.port):"https"===this.protocol()?443:80},e.prototype.protocol=function(){return location.protocol.replace(/:/g,"")},e.prototype.host=function(){return location.hostname},e.prototype.html5Mode=function(){return this._isHtml5},e.prototype.hashPrefix=function(e){return k(e)?this._hashPrefix=e:this._hashPrefix},e.prototype.baseHref=function(e){return k(e)&&(this._baseHref=e),b(this._baseHref)&&(this._baseHref=this.getBaseHref()),this._baseHref},e.prototype.getBaseHref=function(){var e=document.getElementsByTagName("base")[0];return e&&e.href?e.href.replace(/^(https?:)?\/\/[^/]*/,""):location.pathname||"/"},e.prototype.dispose=function(){},e}();function gr(e){return V.$injector=Zn,{name:"vanilla.services",$q:V.$q=Wn,$injector:Zn,dispose:function(){return null}}}var mr=nr("vanilla.hashBangLocation",!1,lr,fr),vr=nr("vanilla.pushStateLocation",!0,pr,fr),yr=nr("vanilla.memoryLocation",!1,cr,hr),wr=function(){function e(){}return e.prototype.dispose=function(e){},e}(),br=Object.freeze({root:F,fromJson:j,toJson:H,forEach:Y,extend:N,equals:q,identity:U,noop:z,createProxyFunctions:B,inherit:W,inArray:G,_inArray:K,removeFrom:Q,_removeFrom:Z,pushTo:X,_pushTo:J,deregAll:ee,defaults:te,mergeR:ne,ancestors:re,pick:ie,omit:oe,pluck:ae,filter:se,find:le,mapObj:ue,map:ce,values:de,allTrueR:pe,anyTrueR:he,unnestR:fe,flattenR:ge,pushR:me,uniqR:ve,unnest:ye,flatten:we,assertPredicate:be,assertMap:$e,assertFn:_e,pairs:Ce,arrayTuples:Se,applyPairs:ke,tail:De,copy:xe,_extend:Oe,silenceUncaughtInPromise:Ae,silentRejection:Pe,notImplemented:I,services:V,Glob:Me,curry:u,compose:n,pipe:l,prop:w,propEq:v,parse:S,not:d,and:r,or:i,all:c,any:p,is:h,eq:o,val:f,invoke:a,pattern:m,isUndefined:b,isDefined:k,isNull:$,isNullOrUndefined:_,isFunction:D,isNumber:x,isString:O,isObject:T,isArray:E,isDate:A,isRegExp:P,isInjectable:M,isPromise:R,Queue:Re,maxLength:Fe,padString:Le,kebobString:je,functionToString:He,fnToString:Ye,stringify:Ue,beforeAfterSubstr:ze,hostRegex:Be,stripLastPathElement:We,splitHash:Ge,splitQuery:Ke,splitEqual:Qe,trimHashVal:Ze,splitOnDelim:Xe,joinNeighborsR:Je,get Category(){return g.Category},Trace:lt,trace:ut,get DefType(){return g.DefType},Param:vt,ParamTypes:yt,StateParams:wt,ParamType:ct,PathNode:bt,PathUtils:_t,resolvePolicies:Ct,defaultResolvePolicy:St,Resolvable:kt,NATIVE_INJECTOR_TOKEN:Tt,ResolveContext:Et,resolvablesBuilder:Lt,StateBuilder:Yt,StateObject:Nt,StateMatcher:qt,StateQueueManager:Ut,StateRegistry:zt,StateService:Bn,TargetState:$t,get TransitionHookPhase(){return g.TransitionHookPhase},get TransitionHookScope(){return g.TransitionHookScope},HookBuilder:Zt,matchState:Gt,RegisteredHook:Kt,makeEvent:Qt,get RejectType(){return g.RejectType},Rejection:Ve,Transition:Jt,TransitionHook:Wt,TransitionEventType:Yn,defaultTransOpts:Un,TransitionService:zn,UrlMatcher:rn,ParamFactory:an,UrlMatcherFactory:sn,UrlRouter:dn,UrlRuleFactory:ln,BaseUrlRule:un,UrlService:$n,ViewService:hn,UIRouterGlobals:fn,UIRouter:Cn,$q:Wn,$injector:Zn,BaseLocationServices:ar,HashLocationService:lr,MemoryLocationService:cr,PushStateLocationService:pr,MemoryLocationConfig:hr,BrowserLocationConfig:fr,keyValsToObjectR:Xn,getParams:Jn,parseUrl:er,buildUrl:tr,locationPluginFactory:nr,servicesPlugin:gr,hashLocationPlugin:mr,pushStateLocationPlugin:vr,memoryLocationPlugin:yr,UIRouterPluginBase:wr});function $r(){var n=null;return function(e,t){return n=n||V.$injector.get("$templateFactory"),[new kr(e,t,n)]}}var _r=function(e,n){return e.reduce(function(e,t){return e||k(n[t])},!1)};function Cr(r){if(!r.parent)return{};var i=["component","bindings","componentProvider"],o=["templateProvider","templateUrl","template","notify","async"].concat(["controller","controllerProvider","controllerAs","resolveAs"]),e=i.concat(o);if(k(r.views)&&_r(e,r))throw new Error("State '"+r.name+"' has a 'views' object. It cannot also have \"view properties\" at the state level. Move the following properties into a view (in the 'views' object): "+e.filter(function(e){return k(r[e])}).join(", "));var a={},t=r.views||{$default:ie(r,e)};return Y(t,function(e,t){if(t=t||"$default",O(e)&&(e={component:e}),e=N({},e),_r(i,e)&&_r(o,e))throw new Error("Cannot combine: "+i.join("|")+" with: "+o.join("|")+" in stateview: '"+t+"@"+r.name+"'");e.resolveAs=e.resolveAs||"$resolve",e.$type="ng1",e.$context=r,e.$name=t;var n=hn.normalizeUIViewTarget(e.$context,e.$name);e.$uiViewName=n.uiViewName,e.$uiViewContextAnchor=n.uiViewContextAnchor,a[t]=e}),a}var Sr=0,kr=function(){function e(e,t,n){var r=this;this.path=e,this.viewDecl=t,this.factory=n,this.$id=Sr++,this.loaded=!1,this.getTemplate=function(e,t){return r.component?r.factory.makeComponentTemplate(e,t,r.component,r.viewDecl.bindings):r.template}}return e.prototype.load=function(){var t=this,e=V.$q,n=new Et(this.path),r=this.path.reduce(function(e,t){return N(e,t.paramValues)},{}),i={template:e.when(this.factory.fromConfig(this.viewDecl,r,n)),controller:e.when(this.getController(n))};return e.all(i).then(function(e){return ut.traceViewServiceEvent("Loaded",t),t.controller=e.controller,N(t,e.template),t})},e.prototype.getController=function(e){var t=this.viewDecl.controllerProvider;if(!M(t))return this.viewDecl.controller;var n=V.$injector.annotate(t),r=E(t)?De(t):t;return new kt("",r,n).get(e)},e}(),Dr=function(){function e(){var r=this;this._useHttp=C.version.minor<3,this.$get=["$http","$templateCache","$injector",function(e,t,n){return r.$templateRequest=n.has&&n.has("$templateRequest")&&n.get("$templateRequest"),r.$http=e,r.$templateCache=t,r}]}return e.prototype.useHttpService=function(e){this._useHttp=e},e.prototype.fromConfig=function(e,t,n){var r=function(e){return V.$q.when(e).then(function(e){return{template:e}})},i=function(e){return V.$q.when(e).then(function(e){return{component:e}})};return k(e.template)?r(this.fromString(e.template,t)):k(e.templateUrl)?r(this.fromUrl(e.templateUrl,t)):k(e.templateProvider)?r(this.fromProvider(e.templateProvider,t,n)):k(e.component)?i(e.component):k(e.componentProvider)?i(this.fromComponentProvider(e.componentProvider,t,n)):r("")},e.prototype.fromString=function(e,t){return D(e)?e(t):e},e.prototype.fromUrl=function(e,t){return D(e)&&(e=e(t)),null==e?null:this._useHttp?this.$http.get(e,{cache:this.$templateCache,headers:{Accept:"text/html"}}).then(function(e){return e.data}):this.$templateRequest(e)},e.prototype.fromProvider=function(e,t,n){var r=V.$injector.annotate(e),i=E(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.fromComponentProvider=function(e,t,n){var r=V.$injector.annotate(e),i=E(e)?De(e):e;return new kt("",i,r).get(n)},e.prototype.makeComponentTemplate=function(l,u,e,c){c=c||{};var d=3<=C.version.minor?"::":"",p=function(e){var t=je(e);return/^(x|data)-/.exec(t)?"x-"+t:t},t=function(e){var t=V.$injector.get(e+"Directive");if(!t||!t.length)throw new Error("Unable to find component named '"+e+"'");return t.map(xr).reduce(fe,[])}(e).map(function(e){var t=e.name,n=e.type,r=p(t);if(l.attr(r)&&!c[t])return r+"='"+l.attr(r)+"'";var i=c[t]||t;if("@"===n)return r+"='{{"+d+"$resolve."+i+"}}'";if("&"===n){var o=u.getResolvable(i),a=o&&o.data,s=a&&V.$injector.annotate(a)||[];return r+"='$resolve."+i+(E(a)?"["+(a.length-1)+"]":"")+"("+s.join(",")+")'"}return r+"='"+d+"$resolve."+i+"'"}).join(" "),n=p(e);return"<"+n+" "+t+">"},e}();var xr=function(e){return T(e.bindToController)?Or(e.bindToController):Or(e.scope)},Or=function(t){return Object.keys(t||{}).map(function(e){return[e,/^([=<@&])[?]?(.*)/.exec(t[e])]}).filter(function(e){return k(e)&&E(e[1])}).map(function(e){return{name:e[1][2]||e[0],type:e[1][1]}})},Tr=function(){function n(e,t){this.stateRegistry=e,this.stateService=t,B(f(n.prototype),this,f(this))}return n.prototype.decorator=function(e,t){return this.stateRegistry.decorator(e,t)||this},n.prototype.state=function(e,t){return T(e)?t=e:t.name=e,this.stateRegistry.register(t),this},n.prototype.onInvalid=function(e){return this.stateService.onInvalid(e)},n}(),Er=function(n){return function(e,t){var i=e[n],o="onExit"===n?"from":"to";return i?function(e,t){var n=new Et(e.treeChanges(o)).subContext(t.$$state()),r=N(Wr(n),{$state$:t,$transition$:e});return V.$injector.invoke(i,this,r)}:void 0}},Ar=function(){function e(e){this._urlListeners=[],this.$locationProvider=e;var t=f(e);B(t,this,t,["hashPrefix"])}return e.monkeyPatchPathParameterType=function(e){var t=e.urlMatcherFactory.type("path");t.encode=function(e){return null!=e?e.toString().replace(/(~|\/)/g,function(e){return{"~":"~~","/":"~2F"}[e]}):e},t.decode=function(e){return null!=e?e.toString().replace(/(~~|~2F)/g,function(e){return{"~~":"~","~2F":"/"}[e]}):e}},e.prototype.dispose=function(){},e.prototype.onChange=function(e){var t=this;return this._urlListeners.push(e),function(){return Q(t._urlListeners)(e)}},e.prototype.html5Mode=function(){var e=this.$locationProvider.html5Mode();return(e=T(e)?e.enabled:e)&&this.$sniffer.history},e.prototype.baseHref=function(){return this._baseHref||(this._baseHref=this.$browser.baseHref()||this.$window.location.pathname)},e.prototype.url=function(e,t,n){return void 0===t&&(t=!1),k(e)&&this.$location.url(e),t&&this.$location.replace(),n&&this.$location.state(n),this.$location.url()},e.prototype._runtimeServices=function(e,t,n,r,i){var o=this;this.$location=t,this.$sniffer=n,this.$browser=r,this.$window=i,e.$on("$locationChangeSuccess",function(t){return o._urlListeners.forEach(function(e){return e(t)})});var a=f(t);B(a,this,a,["replace","path","search","hash"]),B(a,this,a,["port","protocol","host"])},e}(),Pr=function(){function n(e){this._router=e,this._urlRouter=e.urlRouter}return n.injectableHandler=function(t,n){return function(e){return V.$injector.invoke(n,null,{$match:e,$stateParams:t.globals.params})}},n.prototype.$get=function(){var e=this._urlRouter;return e.update(!0),e.interceptDeferred||e.listen(),e},n.prototype.rule=function(e){var t=this;if(!D(e))throw new Error("'rule' must be a function");var n=new un(function(){return e(V.$injector,t._router.locationService)},U);return this._urlRouter.rule(n),this},n.prototype.otherwise=function(e){var t=this,n=this._urlRouter;if(O(e))n.otherwise(e);else{if(!D(e))throw new Error("'rule' must be a string or function");n.otherwise(function(){return e(V.$injector,t._router.locationService)})}return this},n.prototype.when=function(e,t){return(E(t)||D(t))&&(t=n.injectableHandler(this._router,t)),this._urlRouter.when(e,t),this},n.prototype.deferIntercept=function(e){this._urlRouter.deferIntercept(e)},n}();C.module("ui.router.angular1",[]);var Mr=C.module("ui.router.init",["ng"]),Rr=C.module("ui.router.util",["ui.router.init"]),Ir=C.module("ui.router.router",["ui.router.util"]),Vr=C.module("ui.router.state",["ui.router.router","ui.router.util","ui.router.angular1"]),Fr=C.module("ui.router",["ui.router.init","ui.router.state","ui.router.angular1"]),Lr=(C.module("ui.router.compat",["ui.router"]),null);function jr(e){(Lr=this.router=new Cn).stateProvider=new Tr(Lr.stateRegistry,Lr.stateService),Lr.stateRegistry.decorator("views",Cr),Lr.stateRegistry.decorator("onExit",Er("onExit")),Lr.stateRegistry.decorator("onRetain",Er("onRetain")),Lr.stateRegistry.decorator("onEnter",Er("onEnter")),Lr.viewService._pluginapi._viewConfigFactory("ng1",$r());var s=Lr.locationService=Lr.locationConfig=new Ar(e);function t(e,t,n,r,i,o,a){return s._runtimeServices(i,e,r,t,n),delete Lr.router,delete Lr.$get,Lr}return Ar.monkeyPatchPathParameterType(Lr),((Lr.router=Lr).$get=t).$inject=["$location","$browser","$window","$sniffer","$rootScope","$http","$templateCache"],Lr}jr.$inject=["$locationProvider"];var Hr=function(n){return["$uiRouterProvider",function(e){var t=e.router[n];return t.$get=function(){return t},t}]};function Yr(t,e,n){if(V.$injector=t,V.$q=e,!t.hasOwnProperty("strictDi"))try{t.invoke(function(e){})}catch(e){t.strictDi=!!/strict mode/.exec(e&&e.toString())}n.stateRegistry.get().map(function(e){return e.$$state().resolvables}).reduce(fe,[]).filter(function(e){return"deferred"===e.deps}).forEach(function(e){return e.deps=t.annotate(e.resolveFn,t.strictDi)})}Yr.$inject=["$injector","$q","$uiRouter"];function Nr(e){e.$watch(function(){ut.approximateDigests++})}Nr.$inject=["$rootScope"],Mr.provider("$uiRouter",jr),Ir.provider("$urlRouter",["$uiRouterProvider",function(e){return e.urlRouterProvider=new Pr(e)}]),Rr.provider("$urlService",Hr("urlService")),Rr.provider("$urlMatcherFactory",["$uiRouterProvider",function(){return Lr.urlMatcherFactory}]),Rr.provider("$templateFactory",function(){return new Dr}),Vr.provider("$stateRegistry",Hr("stateRegistry")),Vr.provider("$uiRouterGlobals",Hr("globals")),Vr.provider("$transitions",Hr("transitionService")),Vr.provider("$state",["$uiRouterProvider",function(){return N(Lr.stateProvider,{$get:function(){return Lr.stateService}})}]),Vr.factory("$stateParams",["$uiRouter",function(e){return e.globals.params}]),Fr.factory("$view",function(){return Lr.viewService}),Fr.service("$trace",function(){return ut}),Fr.run(Nr),Rr.run(["$urlMatcherFactory",function(e){}]),Vr.run(["$state",function(e){}]),Ir.run(["$urlRouter",function(e){}]),Mr.run(Yr);var qr,Ur,zr,Br,Wr=function(n){return n.getTokens().filter(O).map(function(e){var t=n.getResolvable(e);return[e,"NOWAIT"===n.getPolicy(t).async?t.promise:t.data]}).reduce(ke,{})};function Gr(e){var t,n=e.match(/^\s*({[^}]*})\s*$/);if(n&&(e="("+n[1]+")"),!(t=e.replace(/\n/g," ").match(/^\s*([^(]*?)\s*(\((.*)\))?\s*$/))||4!==t.length)throw new Error("Invalid state ref '"+e+"'");return{state:t[1]||null,paramExpr:t[3]||null}}function Kr(e){var t=e.parent().inheritedData("$uiView"),n=S("$cfg.path")(t);return n?De(n).state.name:void 0}function Qr(e,t,n){var r,i=n.uiState||e.current.name,o=N((r=e,{relative:Kr(t)||r.$current,inherit:!0,source:"sref"}),n.uiStateOpts||{}),a=e.href(i,n.uiStateParams,o);return{uiState:i,uiStateParams:n.uiStateParams,uiStateOpts:o,href:a}}function Zr(e){var t="[object SVGAnimatedString]"===Object.prototype.toString.call(e.prop("href")),n="FORM"===e[0].nodeName;return{attr:n?"action":t?"xlink:href":"href",isAnchor:"A"===e.prop("tagName").toUpperCase(),clickable:!n}}function Xr(o,a,s,l,u){return function(e){var t=e.which||e.button,n=u();if(!(1>>0;if(0===i)return-1;var o=+t||0;if(Math.abs(o)===1/0&&(o=0),i<=o)return-1;for(n=Math.max(0<=o?o:i-Math.abs(o),0);n
    ',this.loadingBarTemplate='
    ',this.$get=["$injector","$document","$timeout","$rootScope",function(i,o,a,s){function l(e){if(m){var t=100*e+"%";f.css("width",t),v=e,y&&(a.cancel(c),c=a(function(){n()},250))}}function n(){if(!(1<=r())){var e,t=r();e=0<=t&&t<.25?(3*Math.random()+3)/100:.25<=t&&t<.65?3*Math.random()/100:.65<=t&&t<.9?2*Math.random()/100:.9<=t&&t<.99?.005:0,l(r()+e)}}function r(){return v}function t(){v=0,m=!1}var u,c,d,p=this.parentSelector,h=angular.element(this.loadingBarTemplate),f=h.find("div").eq(0),g=angular.element(this.spinnerTemplate),m=!1,v=0,y=this.autoIncrement,w=this.includeSpinner,b=this.includeBar,$=this.startSize;return{start:function(){if(u||(u=i.get("$animate")),a.cancel(d),!m){var e=o[0],t=e.querySelector?e.querySelector(p):o.find(p)[0];t||(t=e.getElementsByTagName("body")[0]);var n=angular.element(t),r=t.lastChild&&angular.element(t.lastChild);s.$broadcast("cfpLoadingBar:started"),m=!0,b&&u.enter(h,n,r),w&&u.enter(g,n,h),l($)}},set:l,status:r,inc:n,complete:function(){u||(u=i.get("$animate")),s.$broadcast("cfpLoadingBar:completed"),l(1),a.cancel(d),d=a(function(){var e=u.leave(h,t);e&&e.then&&e.then(t),u.leave(g)},500)},autoIncrement:this.autoIncrement,includeSpinner:this.includeSpinner,latencyThreshold:this.latencyThreshold,parentSelector:this.parentSelector,startSize:this.startSize}}]})}(),angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,s,l){function e(e){for(var t in e)if(void 0!==n.style[t])return e[t]}var u=function(e,t,n){n=n||{};var r=a.defer(),i=u[n.animation?"animationEndEventName":"transitionEndEventName"],o=function(){l.$apply(function(){e.unbind(i,o),r.resolve(e)})};return i&&e.bind(i,o),s(function(){angular.isString(t)?e.addClass(t):angular.isFunction(t)?t(e):angular.isObject(t)&&e.css(t),i||r.resolve(e)}),r.promise.cancel=function(){i&&e.unbind(i,o),r.reject("Transition cancelled")},r.promise},n=document.createElement("trans");return u.transitionEndEventName=e({WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"}),u.animationEndEventName=e({WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"}),u}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(l){return{link:function(e,r,t){function n(e){function t(){a===n&&(a=void 0)}var n=l(r,e);return a&&a.cancel(),(a=n).then(t,t),n}function i(){r.removeClass("collapsing"),r.addClass("collapse in"),r.css({height:"auto"})}function o(){r.removeClass("collapsing"),r.addClass("collapse")}var a,s=!0;e.$watch(t.collapse,function(e){e?s?(s=!1,o(),r.css({height:0})):(r.css({height:r[0].scrollHeight+"px"}),r[0].offsetWidth,r.removeClass("collapse in").addClass("collapsing"),n({height:0}).then(o)):s?(s=!1,i()):(r.removeClass("collapse").addClass("collapsing"),n({height:r[0].scrollHeight+"px"}).then(i))})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(e,n,r){this.groups=[],this.closeOthers=function(t){(angular.isDefined(n.closeOthers)?e.$eval(n.closeOthers):r.closeOthers)&&angular.forEach(this.groups,function(e){e!==t&&(e.isOpen=!1)})},this.addGroup=function(e){var t=this;this.groups.push(e),e.$on("$destroy",function(){t.removeGroup(e)})},this.removeGroup=function(e){var t=this.groups.indexOf(e);-1!==t&&this.groups.splice(t,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(e){this.heading=e}},link:function(t,e,n,r){r.addGroup(t),t.$watch("isOpen",function(e){e&&r.closeOthers(t)}),t.toggleOpen=function(){t.isDisabled||(t.isOpen=!t.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(e,t,n,r,i){r.setHeading(i(e,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(e,t,n,r){e.$watch(function(){return r[n.accordionTransclude]},function(e){e&&(t.html(""),t.append(e))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(e,t){e.closeable="close"in t,this.close=e.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}).directive("dismissOnTimeout",["$timeout",function(i){return{require:"alert",link:function(e,t,n,r){i(function(){r.close()},parseInt(n.dismissOnTimeout,10))}}}]),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(e,t,n){t.addClass("ng-binding").data("$binding",n.bindHtmlUnsafe),e.$watch(n.bindHtmlUnsafe,function(e){t.html(e||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(e){this.activeClass=e.activeClass||"active",this.toggleEvent=e.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(t,n,r,e){var i=e[0],o=e[1];o.$render=function(){n.toggleClass(i.activeClass,angular.equals(o.$modelValue,t.$eval(r.btnRadio)))},n.bind(i.toggleEvent,function(){var e=n.hasClass(i.activeClass);(!e||angular.isDefined(r.uncheckable))&&t.$apply(function(){o.$setViewValue(e?null:t.$eval(r.btnRadio)),o.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(r,e,t,n){function i(){return o(t.btnCheckboxTrue,!0)}function o(e,t){var n=r.$eval(e);return angular.isDefined(n)?n:t}var a=n[0],s=n[1];s.$render=function(){e.toggleClass(a.activeClass,angular.equals(s.$modelValue,i()))},e.bind(a.toggleEvent,function(){r.$apply(function(){s.$setViewValue(e.hasClass(a.activeClass)?o(t.btnCheckboxFalse,!1):i()),s.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$interval","$transition",function(a,t,n,s){function l(){r();var e=+a.interval;!isNaN(e)&&0=d.length?d[t-1]:d[t]):t
    ");e.attr({"ng-model":"date","ng-change":"dateSelection()"});var c=angular.element(e.children()[0]);i.datepickerOptions&&angular.forEach(r.$parent.$eval(i.datepickerOptions),function(e,t){c.attr(o(t),e)}),r.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(t){if(i[t]){var e=g(i[t]);if(r.$parent.$watch(e,function(e){r.watchData[t]=e}),c.attr(o(t),"watchData."+t),"datepickerMode"===t){var n=e.assign;r.$watch("watchData."+t,function(e,t){e!==t&&n(r.$parent,e)})}}}),i.dateDisabled&&c.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),n.$parsers.unshift(a),r.dateSelection=function(e){angular.isDefined(e)&&(r.date=e),n.$setViewValue(r.date),n.$render(),l&&(r.isOpen=!1,t[0].focus())},t.bind("input change keyup",function(){r.$apply(function(){r.date=n.$modelValue})}),n.$render=function(){var e=n.$viewValue?y(n.$viewValue,s):"";t.val(e),r.date=a(n.$modelValue)};var d=function(e){r.isOpen&&e.target!==t[0]&&r.$apply(function(){r.isOpen=!1})},p=function(e){r.keydown(e)};t.bind("keydown",p),r.keydown=function(e){27===e.which?(e.preventDefault(),e.stopPropagation(),r.close()):40!==e.which||r.isOpen||(r.isOpen=!0)},r.$watch("isOpen",function(e){e?(r.$broadcast("datepicker.focus"),r.position=u?v.offset(t):v.position(t),r.position.top=r.position.top+t.prop("offsetHeight"),m.bind("click",d)):m.unbind("click",d)}),r.select=function(e){if("today"===e){var t=new Date;angular.isDate(n.$modelValue)?(e=new Date(n.$modelValue)).setFullYear(t.getFullYear(),t.getMonth(),t.getDate()):e=new Date(t.setHours(0,0,0,0))}r.dateSelection(e)},r.close=function(){r.isOpen=!1,t[0].focus()};var h=f(e)(r);e.remove(),u?m.find("body").append(h):t.after(h),r.$on("$destroy",function(){h.remove(),t.unbind("keydown",p),m.unbind("click",d)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(e,t){t.bind("click",function(e){e.preventDefault(),e.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(t){var n=null;this.open=function(e){n||(t.bind("click",r),t.bind("keydown",i)),n&&n!==e&&(n.isOpen=!1),n=e},this.close=function(e){n===e&&(n=null,t.unbind("click",r),t.unbind("keydown",i))};var r=function(e){if(n){var t=n.getToggleElement();e&&t&&t[0].contains(e.target)||n.$apply(function(){n.isOpen=!1})}},i=function(e){27===e.which&&(n.focusToggleElement(),r())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(n,t,r,e,i,o){var a,s=this,l=n.$new(),u=e.openClass,c=angular.noop,d=t.onToggle?r(t.onToggle):angular.noop;this.init=function(e){s.$element=e,t.isOpen&&(a=r(t.isOpen),c=a.assign,n.$watch(a,function(e){l.isOpen=!!e}))},this.toggle=function(e){return l.isOpen=arguments.length?!!e:!l.isOpen},this.isOpen=function(){return l.isOpen},l.getToggleElement=function(){return s.toggleElement},l.focusToggleElement=function(){s.toggleElement&&s.toggleElement[0].focus()},l.$watch("isOpen",function(e,t){o[e?"addClass":"removeClass"](s.$element,u),e?(l.focusToggleElement(),i.open(l)):i.close(l),c(n,e),angular.isDefined(e)&&e!==t&&d(n,{open:!!e})}),n.$on("$locationChangeSuccess",function(){l.isOpen=!1}),n.$on("$destroy",function(){l.$destroy()})}]).directive("dropdown",function(){return{controller:"DropdownController",link:function(e,t,n,r){r.init(t)}}}).directive("dropdownToggle",function(){return{require:"?^dropdown",link:function(t,n,r,i){if(i){i.toggleElement=n;var e=function(e){e.preventDefault(),n.hasClass("disabled")||r.disabled||t.$apply(function(){i.toggle()})};n.bind("click",e),n.attr({"aria-haspopup":!0,"aria-expanded":!1}),t.$watch(i.isOpen,function(e){n.attr("aria-expanded",!!e)}),t.$on("$destroy",function(){n.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var r=[];return{add:function(e,t){r.push({key:e,value:t})},get:function(e){for(var t=0;t");i.attr("backdrop-class",t.backdropClass),h=c(i)(f),n.append(h)}var o=angular.element("
    ");o.attr({"template-url":t.windowTemplateUrl,"window-class":t.windowClass,size:t.size,index:m.length()-1,animate:"animate"}).html(t.content);var a=c(o)(t.scope);m.top().value.modalDomEl=a,n.append(a),n.addClass(g)},n.close=function(e,t){var n=m.get(e);n&&(n.value.deferred.resolve(t),r(e))},n.dismiss=function(e,t){var n=m.get(e);n&&(n.value.deferred.reject(t),r(e))},n.dismissAll=function(e){for(var t=this.getTop();t;)this.dismiss(t.key,e),t=this.getTop()},n.getTop=function(){return m.top()},n}]).provider("$modal",function(){var g={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(l,u,c,d,p,h,f){var e={};return e.open=function(o){var a=c.defer(),e=c.defer(),s={result:a.promise,opened:e.promise,close:function(e){f.close(s,e)},dismiss:function(e){f.dismiss(s,e)}};if((o=angular.extend({},g.options,o)).resolve=o.resolve||{},!o.template&&!o.templateUrl)throw new Error("One of template or templateUrl options is required.");var t,n,r,i=c.all([(r=o,r.template?c.when(r.template):d.get(angular.isFunction(r.templateUrl)?r.templateUrl():r.templateUrl,{cache:p}).then(function(e){return e.data}))].concat((t=o.resolve,n=[],angular.forEach(t,function(e){(angular.isFunction(e)||angular.isArray(e))&&n.push(c.when(l.invoke(e)))}),n)));return i.then(function(n){var e=(o.scope||u).$new();e.$close=s.close,e.$dismiss=s.dismiss;var t,r={},i=1;o.controller&&(r.$scope=e,r.$modalInstance=s,angular.forEach(o.resolve,function(e,t){r[t]=n[i++]}),t=h(o.controller,r),o.controllerAs&&(e[o.controllerAs]=t)),f.open(s,{scope:e,deferred:a,content:n[0],backdrop:o.backdrop,keyboard:o.keyboard,backdropClass:o.backdropClass,windowClass:o.windowClass,windowTemplateUrl:o.windowTemplateUrl,size:o.size})},function(e){a.reject(e)}),i.then(function(){e.resolve(!0)},function(){e.reject(!1)}),s},e}]};return g}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(n,r,i){var o=this,a={$setViewValue:angular.noop},t=r.numPages?i(r.numPages).assign:angular.noop;this.init=function(e,t){a=e,this.config=t,a.$render=function(){o.render()},r.itemsPerPage?n.$parent.$watch(i(r.itemsPerPage),function(e){o.itemsPerPage=parseInt(e,10),n.totalPages=o.calculateTotalPages()}):this.itemsPerPage=t.itemsPerPage},this.calculateTotalPages=function(){var e=this.itemsPerPage<1?1:Math.ceil(n.totalItems/this.itemsPerPage);return Math.max(e||0,1)},this.render=function(){n.page=parseInt(a.$viewValue,10)||1},n.selectPage=function(e){n.page!==e&&0e?n.selectPage(e):a.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(s,l){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(e,t,n,r){function c(e,t,n){return{number:e,text:t,active:n}}var i=r[0],o=r[1];if(o){var d=angular.isDefined(n.maxSize)?e.$parent.$eval(n.maxSize):l.maxSize,p=angular.isDefined(n.rotate)?e.$parent.$eval(n.rotate):l.rotate;e.boundaryLinks=angular.isDefined(n.boundaryLinks)?e.$parent.$eval(n.boundaryLinks):l.boundaryLinks,e.directionLinks=angular.isDefined(n.directionLinks)?e.$parent.$eval(n.directionLinks):l.directionLinks,i.init(o,l),n.maxSize&&e.$parent.$watch(s(n.maxSize),function(e){d=parseInt(e,10),i.render()});var a=i.render;i.render=function(){a(),0';return{restrict:"EA",compile:function(){var _=o(i);return function(r,t,i){function e(){m.isOpen?o():n()}function n(){var e,t,n;(!g||r.$eval(i[S+"Enable"]))&&(n=i[S+"Placement"],m.placement=angular.isDefined(n)?n:D.placement,e=i[S+"PopupDelay"],t=parseInt(e,10),m.popupDelay=isNaN(t)?D.popupDelay:t,m.popupDelay?p||(p=x(a,m.popupDelay,!1)).then(function(e){e()}):a()())}function o(){r.$apply(function(){s()})}function a(){return p=null,d&&(x.cancel(d),d=null),m.content?(u&&l(),c=m.$new(),(u=_(c,function(e){h?O.find("body").append(e):t.after(e)})).css({top:0,left:0,display:"block"}),m.$digest(),v(),m.isOpen=!0,m.$digest(),v):angular.noop}function s(){m.isOpen=!1,x.cancel(p),p=null,m.animation?d||(d=x(l,500)):l()}function l(){d=null,u&&(u.remove(),u=null),c&&(c.$destroy(),c=null)}var u,c,d,p,h=!!angular.isDefined(D.appendToBody)&&D.appendToBody,f=k(void 0),g=angular.isDefined(i[S+"Enable"]),m=r.$new(!0),v=function(){var e=T.positionElements(t,u,m.placement,h);e.top+="px",e.left+="px",u.css(e)};m.isOpen=!1,i.$observe(C,function(e){!(m.content=e)&&m.isOpen&&s()}),i.$observe(S+"Title",function(e){m.title=e});var y,w=function(){t.unbind(f.show,n),t.unbind(f.hide,o)};y=i[S+"Trigger"],w(),(f=k(y)).show===f.hide?t.bind(f.show,e):(t.bind(f.show,n),t.bind(f.hide,o));var b=r.$eval(i[S+"Animation"]);m.animation=angular.isDefined(b)?!!b:D.animation;var $=r.$eval(i[S+"AppendToBody"]);(h=angular.isDefined($)?$:h)&&r.$on("$locationChangeSuccess",function(){m.isOpen&&s()}),r.$on("$destroy",function(){x.cancel(d),x.cancel(p),w(),l(),m=null})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(e){return e("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(e){return e("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(e){return e("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(n,e,t){var r=this,i=angular.isDefined(e.animate)?n.$parent.$eval(e.animate):t.animate;this.bars=[],n.max=angular.isDefined(e.max)?n.$parent.$eval(e.max):t.max,this.addBar=function(t,e){i||e.css({transition:"none"}),this.bars.push(t),t.$watch("value",function(e){t.percent=+(100*e/n.max).toFixed(2)}),t.$on("$destroy",function(){e=null,r.removeBar(t)})},this.removeBar=function(e){this.bars.splice(this.bars.indexOf(e),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(e,t,n,r){r.addBar(e,t)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(e,t,n,r){r.addBar(e,angular.element(t.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(n,r,i){var o={$setViewValue:angular.noop};this.init=function(e){(o=e).$render=this.render,this.stateOn=angular.isDefined(r.stateOn)?n.$parent.$eval(r.stateOn):i.stateOn,this.stateOff=angular.isDefined(r.stateOff)?n.$parent.$eval(r.stateOff):i.stateOff;var t=angular.isDefined(r.ratingStates)?n.$parent.$eval(r.ratingStates):new Array(angular.isDefined(r.max)?n.$parent.$eval(r.max):i.max);n.range=this.buildTemplateObjects(t)},this.buildTemplateObjects=function(e){for(var t=0,n=e.length;t");v.attr({id:t,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(e.typeaheadTemplateUrl)&&v.attr("template-url",e.typeaheadTemplateUrl);var y=function(){m.matches=[],m.activeIdx=-1,a.attr("aria-expanded",!1)},w=function(e){return t+"-option-"+e};m.$watch("activeIdx",function(e){e<0?a.removeAttr("aria-activedescendant"):a.attr("aria-activedescendant",w(e))});var b=function(r){var i={$viewValue:r};u(o,!0),x.when(g.source(o,i)).then(function(e){var t=r===s.$viewValue;if(t&&l)if(0=n?0$&"):e}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion-group.html",'
    \n
    \n

    \n {{heading}}\n

    \n
    \n
    \n\t
    \n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(e){e.put("template/accordion/accordion.html",'
    ')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(e){e.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(e){e.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(e){e.put("template/carousel/slide.html","
    \n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/datepicker.html",'
    \n \n \n \n
    ')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{label.abbr}}
    {{ weekNumbers[$index] }}\n \n
    \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(e){e.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(e){e.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(e){e.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(e){e.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-html-unsafe-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(e){e.put("template/tooltip/tooltip-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(e){e.put("template/popover/popover.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/bar.html",'
    ')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progress.html",'
    ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(e){e.put("template/progressbar/progressbar.html",'
    \n
    \n
    ')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(e){e.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(e){e.put("template/tabs/tabset.html",'
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(e){e.put("template/timepicker/timepicker.html",'\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
     
    \n\t\t\t\t\n\t\t\t:\n\t\t\t\t\n\t\t\t
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(e){e.put("template/typeahead/typeahead-popup.html",'\n')}]),function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function p(){return e.apply(null,arguments)}function s(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function l(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function u(e){return void 0===e}function c(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function h(e,t){var n,r=[];for(n=0;n>>0,r=0;rCe(e)?(o=e+1,a=s-Ce(e)):(o=e,a=s),{year:o,dayOfYear:a}}function Ne(e,t,n){var r,i,o=He(e.year(),t,n),a=Math.floor((e.dayOfYear()-o-1)/7)+1;return a<1?r=a+qe(i=e.year()-1,t,n):a>qe(e.year(),t,n)?(r=a-qe(e.year(),t,n),i=e.year()+1):(i=e.year(),r=a),{week:r,year:i}}function qe(e,t,n){var r=He(e,t,n),i=He(e+1,t,n);return(Ce(e)-r+i)/7}N("w",["ww",2],"wo","week"),N("W",["WW",2],"Wo","isoWeek"),P("week","w"),P("isoWeek","W"),V("week",5),V("isoWeek",5),le("w",Q),le("ww",Q,B),le("W",Q),le("WW",Q,B),he(["w","ww","W","WW"],function(e,t,n,r){t[r.substr(0,1)]=S(e)});N("d",0,"do","day"),N("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),N("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),N("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),N("e",0,0,"weekday"),N("E",0,0,"isoWeekday"),P("day","d"),P("weekday","e"),P("isoWeekday","E"),V("day",11),V("weekday",11),V("isoWeekday",11),le("d",Q),le("e",Q),le("E",Q),le("dd",function(e,t){return t.weekdaysMinRegex(e)}),le("ddd",function(e,t){return t.weekdaysShortRegex(e)}),le("dddd",function(e,t){return t.weekdaysRegex(e)}),he(["dd","ddd","dddd"],function(e,t,n,r){var i=n._locale.weekdaysParse(e,r,n._strict);null!=i?t.d=i:v(n).invalidWeekday=e}),he(["d","e","E"],function(e,t,n,r){t[r]=S(e)});var Ue="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Be="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var We=ae;var Ge=ae;var Ke=ae;function Qe(){function e(e,t){return t.length-e.length}var t,n,r,i,o,a=[],s=[],l=[],u=[];for(t=0;t<7;t++)n=m([2e3,1]).day(t),r=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),o=this.weekdays(n,""),a.push(r),s.push(i),l.push(o),u.push(r),u.push(i),u.push(o);for(a.sort(e),s.sort(e),l.sort(e),u.sort(e),t=0;t<7;t++)s[t]=ce(s[t]),l[t]=ce(l[t]),u[t]=ce(u[t]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Ze(){return this.hours()%12||12}function Xe(e,t){N(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function Je(e,t){return t._meridiemParse}N("H",["HH",2],0,"hour"),N("h",["hh",2],0,Ze),N("k",["kk",2],0,function(){return this.hours()||24}),N("hmm",0,0,function(){return""+Ze.apply(this)+F(this.minutes(),2)}),N("hmmss",0,0,function(){return""+Ze.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),N("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),N("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),Xe("a",!0),Xe("A",!1),P("hour","h"),V("hour",13),le("a",Je),le("A",Je),le("H",Q),le("h",Q),le("k",Q),le("HH",Q,B),le("hh",Q,B),le("kk",Q,B),le("hmm",Z),le("hmmss",X),le("Hmm",Z),le("Hmmss",X),pe(["H","HH"],ve),pe(["k","kk"],function(e,t,n){var r=S(e);t[ve]=24===r?0:r}),pe(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),pe(["h","hh"],function(e,t,n){t[ve]=S(e),v(n).bigHour=!0}),pe("hmm",function(e,t,n){var r=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r)),v(n).bigHour=!0}),pe("hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r,2)),t[we]=S(e.substr(i)),v(n).bigHour=!0}),pe("Hmm",function(e,t,n){var r=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r))}),pe("Hmmss",function(e,t,n){var r=e.length-4,i=e.length-2;t[ve]=S(e.substr(0,r)),t[ye]=S(e.substr(r,2)),t[we]=S(e.substr(i))});var et,tt=xe("Hours",!0),nt={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Pe,monthsShort:Me,week:{dow:0,doy:6},weekdays:Ue,weekdaysMin:Be,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},rt={},it={};function ot(e){return e?e.toLowerCase().replace("_","-"):e}function at(e){var t=null;if(!rt[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=et._abbr,require("./locale/"+e),st(t)}catch(e){}return rt[e]}function st(e,t){var n;return e&&((n=u(t)?ut(e):lt(e,t))?et=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),et._abbr}function lt(e,t){if(null!==t){var n,r=nt;if(t.abbr=e,null!=rt[e])x("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),r=rt[e]._config;else if(null!=t.parentLocale)if(null!=rt[t.parentLocale])r=rt[t.parentLocale]._config;else{if(null==(n=at(t.parentLocale)))return it[t.parentLocale]||(it[t.parentLocale]=[]),it[t.parentLocale].push({name:e,config:t}),null;r=n._config}return rt[e]=new E(T(r,t)),it[e]&&it[e].forEach(function(e){lt(e.name,e.config)}),st(e),rt[e]}return delete rt[e],null}function ut(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return et;if(!s(e)){if(t=at(e))return t;e=[e]}return function(e){for(var t,n,r,i,o=0;o=t&&a(i,n,!0)>=t-1)break;t--}o++}return et}(e)}function ct(e){var t,n=e._a;return n&&-2===v(e).overflow&&(t=n[ge]<0||11Ee(n[fe],n[ge])?me:n[ve]<0||24qe(n,o,a)?v(e)._overflowWeeks=!0:null!=l?v(e)._overflowWeekday=!0:(s=Ye(n,r,i,o,a),e._a[fe]=s.year,e._dayOfYear=s.dayOfYear)}(e),null!=e._dayOfYear&&(o=dt(e._a[fe],r[fe]),(e._dayOfYear>Ce(o)||0===e._dayOfYear)&&(v(e)._overflowDayOfYear=!0),n=je(o,0,e._dayOfYear),e._a[ge]=n.getUTCMonth(),e._a[me]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=r[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ve]&&0===e._a[ye]&&0===e._a[we]&&0===e._a[be]&&(e._nextDay=!0,e._a[ve]=0),e._d=(e._useUTC?je:function(e,t,n,r,i,o,a){var s=new Date(e,t,n,r,i,o,a);return e<100&&0<=e&&isFinite(s.getFullYear())&&s.setFullYear(e),s}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ve]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(v(e).weekdayMismatch=!0)}}var ht=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ft=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/Z|[+-]\d\d(?::?\d\d)?/,mt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],vt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],yt=/^\/?Date\((\-?\d+)/i;function wt(e){var t,n,r,i,o,a,s=e._i,l=ht.exec(s)||ft.exec(s);if(l){for(v(e).iso=!0,t=0,n=mt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},un.isLocal=function(){return!!this.isValid()&&!this._isUTC},un.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},un.isUtc=Ht,un.isUTC=Ht,un.zoneAbbr=function(){return this._isUTC?"UTC":""},un.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},un.dates=n("dates accessor is deprecated. Use date instead.",nn),un.months=n("months accessor is deprecated. Use month instead",Ie),un.years=n("years accessor is deprecated. Use year instead",De),un.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),un.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!u(this._isDSTShifted))return this._isDSTShifted;var e={};if(b(e,this),(e=kt(e))._a){var t=e._isUTC?m(e._a):xt(e._a);this._isDSTShifted=this.isValid()&&0-1/0&&t.selectable&&c[e]){var r=c[e](t.utcDateValue),i=[];if(r.weeks)for(var o=0;on+9,past:u.year()r.indexOf(e.startView))throw new Error("startView must be greater than minView");if(!a.isNumber(e.minuteStep))throw new Error("minuteStep must be numeric");if(e.minuteStep<=0||60<=e.minuteStep)throw new Error("minuteStep must be greater than zero and less than 60");if(null!==e.configureOn&&!a.isString(e.configureOn))throw new Error("configureOn must be a string");if(null!==e.configureOn&&e.configureOn.length<1)throw new Error("configureOn must not be an empty string");if(null!==e.renderOn&&!a.isString(e.renderOn))throw new Error("renderOn must be a string");if(null!==e.renderOn&&e.renderOn.length<1)throw new Error("renderOn must not be an empty string");if(null!==e.modelType&&!a.isString(e.modelType))throw new Error("modelType must be a string");if(null!==e.modelType&&e.modelType.length<1)throw new Error("modelType must not be an empty string");"Date"!==e.modelType&&"moment"!==e.modelType&&"milliseconds"!==e.modelType&&(e.parseFormat=e.modelType);if(null!==e.dropdownSelector&&!a.isString(e.dropdownSelector))throw new Error("dropdownSelector must be a string");null===e.dropdownSelector||"undefined"!=typeof jQuery&&"function"==typeof jQuery().dropdown||(i.error("Please DO NOT specify the dropdownSelector option unless you are using jQuery AND Bootstrap.js. Please include jQuery AND Bootstrap.js, or write code to close the dropdown in the on-set-time callback. \n\nThe dropdownSelector configuration option is being removed because it will not function properly."),delete e.dropdownSelector)}}}a.module("ui.bootstrap.datetimepicker",[]).service("dateTimePickerConfig",function(){var e={bg:{previous:"предишна",next:"следваща"},ca:{previous:"anterior",next:"següent"},da:{previous:"forrige",next:"næste"},de:{previous:"vorige",next:"weiter"},"en-au":{previous:"previous",next:"next"},"en-gb":{previous:"previous",next:"next"},en:{previous:"previous",next:"next"},"es-us":{previous:"atrás",next:"siguiente"},es:{previous:"atrás",next:"siguiente"},fi:{previous:"edellinen",next:"seuraava"},fr:{previous:"précédent",next:"suivant"},hu:{previous:"előző",next:"következő"},it:{previous:"precedente",next:"successivo"},ja:{previous:"前へ",next:"次へ"},ml:{previous:"മുൻപുള്ളത്",next:"അടുത്തത്"},nl:{previous:"vorige",next:"volgende"},pl:{previous:"poprzednia",next:"następna"},"pt-br":{previous:"anteriores",next:"próximos"},pt:{previous:"anterior",next:"próximo"},ro:{previous:"anterior",next:"următor"},ru:{previous:"предыдущая",next:"следующая"},sk:{previous:"predošlá",next:"ďalšia"},sv:{previous:"föregående",next:"nästa"},tr:{previous:"önceki",next:"sonraki"},uk:{previous:"назад",next:"далі"},"zh-cn":{previous:"上一页",next:"下一页"},"zh-tw":{previous:"上一頁",next:"下一頁"}}[b.locale().toLowerCase()];return a.extend({},{configureOn:null,dropdownSelector:null,minuteStep:5,minView:"minute",modelType:"Date",parseFormat:"YYYY-MM-DDTHH:mm:ss.SSSZZ",renderOn:null,startView:"day"},{screenReader:e})}).service("dateTimePickerValidator",t).directive("datetimepicker",e),e.$inject=["dateTimePickerConfig","dateTimePickerValidator"],t.$inject=["$log"]}),angular.module("rzTable",[]),angular.module("rzTable").directive("rzTable",["resizeStorage","$injector","$parse",function(g,i,u){function e(e){}function c(n,r,i){return function(e,t){!0!==i.busy&&void 0!==t&&t!==e&&(d(n),p(n,r,i))}}function d(e){k=!0,l.map(function(e){e.remove()}),l=[]}function p(e,t,n){if(!n.busy){b=$(e).find("th"),v=n.mode,y=!angular.isDefined(n.saveTableSizes)||n.saveTableSizes,w=n.profile;var r=function(t,e){try{var n=e.rzMode?t.mode:"BasicResizer",r=i.get(n);return r}catch(e){return console.error("The resizer "+t.mode+" was not found"),null}}(n,t);r&&(S=new r(e,b,h),y&&(D=g.loadTableSizes(e,n.mode,n.profile)),s=S.handles(b),a=S.ctrlColumns,S.setup(),(o=D)&&($(C).width("auto"),a.each(function(e,t){var n=angular.element(t).scope(),r=n.rzCol||$(t).attr("id"),i=o[r];$(t).css({width:i})}),S.onTableReady()),s.each(function(e,t){!function(e,t,n){var r=$("
    ",{class:e.options.handleClass||"rz-handle"});$(n).prepend(r),l.push(r);var i=S.handleMiddleware(r,n);p=e,h=r,f=i,$(h).mousedown(function(e){k&&(S.onFirstDrag(f,h),S.onTableReady(),k=!1),p.options.onResizeStarted&&p.options.onResizeStarted(f);var t={};S.intervene&&(((t=S.intervene.selector(f)).column=t).orgWidth=$(t).width()),e.preventDefault(),$(h).addClass(p.options.handleClassActive||"rz-handle-active");var n,r,i,o,a,s,l,u,c=e.clientX,d=$(f).width();o=p,a=f,s=c,l=d,u=t,_=function(e){var t=e.clientX,n=t-s,r=S.calculate(l,n);if(!(rt)||(this.fixedColumn.width()<=this.getMinWidth(this.fixedColumn)?(this.bound=t,$(this.fixedColumn).width(this.minWidth),!0):void 0)},e.prototype.onEndDrag=function(){this.bound=!1},e.prototype.calculate=function(e,t){return e-t},e}]),angular.module("rzTable").factory("OverflowResizer",["ResizerModel",function(r){function e(e,t,n){r.call(this,e,t,n)}return(e.prototype=Object.create(r.prototype)).setup=function(){$(this.container).css({overflow:"auto"})},e.prototype.onTableReady=function(){$(this.table).width(1)},e}]),function(e,t){"function"==typeof define&&define.amd?define(["angular"],t):"object"==typeof module&&module.exports?module.exports=t(require("angular")):e.angularClipboard=t(e.angular)}(this,function(i){return i.module("angular-clipboard",[]).factory("clipboard",["$document","$window",function(s,l){return{copyText:function(e,t){var n,r,i=l.pageXOffset||s[0].documentElement.scrollLeft,o=l.pageYOffset||s[0].documentElement.scrollTop,a=(n=e,(r=s[0].createElement("textarea")).style.position="absolute",r.style.fontSize="12pt",r.style.border="0",r.style.padding="0",r.style.margin="0",r.style.left="-10000px",r.style.top=(l.pageYOffset||s[0].documentElement.scrollTop)+"px",r.textContent=n,r);s[0].body.appendChild(a),function(e){try{s[0].body.style.webkitUserSelect="initial";var t=s[0].getSelection();t.removeAllRanges();var n=document.createRange();n.selectNodeContents(e),t.addRange(n),e.select(),e.setSelectionRange(0,999999);try{if(!s[0].execCommand("copy"))throw"failure copy"}finally{t.removeAllRanges()}}finally{s[0].body.style.webkitUserSelect=""}}(a),l.scrollTo(i,o),s[0].body.removeChild(a)},supported:"queryCommandSupported"in s[0]&&s[0].queryCommandSupported("copy")}}]).directive("clipboard",["clipboard",function(r){return{restrict:"A",scope:{onCopied:"&",onError:"&",text:"=",supported:"=?"},link:function(t,n){t.supported=r.supported,n.on("click",function(e){try{r.copyText(t.text,n[0]),i.isFunction(t.onCopied)&&t.$evalAsync(t.onCopied())}catch(e){i.isFunction(t.onError)&&t.$evalAsync(t.onError({err:e}))}})}}}])}),function(e,t){"function"==typeof define&&define.amd?define("sifter",t):"object"==typeof exports?module.exports=t():e.Sifter=t()}(this,function(){var e=function(e,t){this.items=e,this.settings=t||{diacritics:!0}};e.prototype.tokenize=function(e){if(!(e=s(String(e||"").toLowerCase()))||!e.length)return[];var t,n,r,i,o=[],a=e.split(/ +/);for(t=0,n=a.length;t/g,">").replace(/"/g,""")},t={before:function(e,t,n){var r=e[t];e[t]=function(){return n.apply(e,arguments),r.apply(e,arguments)}},after:function(t,e,n){var r=t[e];t[e]=function(){var e=r.apply(t,arguments);return n.apply(t,arguments),e}}},n=function(t,n,e){var r,i=t.trigger,o={};for(r in t.trigger=function(){var e=arguments[0];if(-1===n.indexOf(e))return i.apply(t,arguments);o[e]=arguments},e.apply(t,[]),t.trigger=i,o)o.hasOwnProperty(r)&&i.apply(t,o[r])},f=function(e){var t={};if("selectionStart"in e)t.start=e.selectionStart,t.length=e.selectionEnd-t.start;else if(document.selection){e.focus();var n=document.selection.createRange(),r=document.selection.createRange().text.length;n.moveStart("character",-e.value.length),t.start=n.text.length-r,t.length=r}return t},O=function(p){var h=null,e=function(e,t){var n,r,i,o,a,s,l,u,c,d;(t=t||{},(e=e||window.event||{}).metaKey||e.altKey)||(t.force||!1!==p.data("grow"))&&(n=p.val(),e.type&&"keydown"===e.type.toLowerCase()&&(i=48<=(r=e.keyCode)&&r<=57||65<=r&&r<=90||96<=r&&r<=111||186<=r&&r<=222||32===r,46===r||8===r?(u=f(p[0])).length?n=n.substring(0,u.start)+n.substring(u.start+u.length):8===r&&u.start?n=n.substring(0,u.start-1)+n.substring(u.start+1):46===r&&void 0!==u.start&&(n=n.substring(0,u.start)+n.substring(u.start+1)):i&&(s=e.shiftKey,l=String.fromCharCode(e.keyCode),n+=l=s?l.toUpperCase():l.toLowerCase())),o=p.attr("placeholder"),!n&&o&&(n=o),d=p,(a=((c=n)?(w.$testInput||(w.$testInput=S("").css({position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}).appendTo("body")),w.$testInput.text(c),function(e,t,n){var r,i,o={};if(n)for(r=0,i=n.length;r").addClass(g.wrapperClass).addClass(s).addClass(a),t=S("
    ").addClass(g.inputClass).addClass("items").appendTo(e),n=S('').appendTo(t).attr("tabindex",w.is(":disabled")?"-1":f.tabIndex),o=S(g.dropdownParent||e),r=S("
    ").addClass(g.dropdownClass).addClass(a).hide().appendTo(o),i=S("
    ").addClass(g.dropdownContentClass).appendTo(r),(u=w.attr("id"))&&(n.attr("id",u+"-selectized"),S("label[for='"+u+"']").attr("for",u+"-selectized")),f.settings.copyClassesToDropdown&&r.addClass(s),e.css({width:w[0].style.width}),f.plugins.names.length&&(l="plugin-"+f.plugins.names.join(" plugin-"),e.addClass(l),r.addClass(l)),(null===g.maxItems||1[data-selectable]",function(e){e.stopImmediatePropagation()}),r.on("mouseenter","[data-selectable]",function(){return f.onOptionHover.apply(f,arguments)}),r.on("mousedown click","[data-selectable]",function(){return f.onOptionSelect.apply(f,arguments)}),d="mousedown",p="*:not(input)",h=function(){return f.onItemSelect.apply(f,arguments)},(c=t).on(d,p,function(e){for(var t=e.target;t&&t.parentNode!==c[0];)t=t.parentNode;return e.currentTarget=t,h.apply(this,[e])}),O(n),t.on({mousedown:function(){return f.onMouseDown.apply(f,arguments)},click:function(){return f.onClick.apply(f,arguments)}}),n.on({mousedown:function(e){e.stopPropagation()},keydown:function(){return f.onKeyDown.apply(f,arguments)},keyup:function(){return f.onKeyUp.apply(f,arguments)},keypress:function(){return f.onKeyPress.apply(f,arguments)},resize:function(){f.positionDropdown.apply(f,[])},blur:function(){return f.onBlur.apply(f,arguments)},focus:function(){return f.ignoreBlur=!1,f.onFocus.apply(f,arguments)},paste:function(){return f.onPaste.apply(f,arguments)}}),y.on("keydown"+m,function(e){f.isCmdDown=e[$?"metaKey":"ctrlKey"],f.isCtrlDown=e[$?"altKey":"ctrlKey"],f.isShiftDown=e.shiftKey}),y.on("keyup"+m,function(e){e.keyCode===C&&(f.isCtrlDown=!1),16===e.keyCode&&(f.isShiftDown=!1),e.keyCode===_&&(f.isCmdDown=!1)}),y.on("mousedown"+m,function(e){if(f.isFocused){if(e.target===f.$dropdown[0]||e.target.parentNode===f.$dropdown[0])return!1;f.$control.has(e.target).length||e.target===f.$control[0]||f.blur(e.target)}}),v.on(["scroll"+m,"resize"+m].join(" "),function(){f.isOpen&&f.positionDropdown.apply(f,arguments)}),v.on("mousemove"+m,function(){f.ignoreHover=!1}),this.revertSettings={$children:w.children().detach(),tabindex:w.attr("tabindex")},w.attr("tabindex",-1).hide().after(f.$wrapper),S.isArray(g.items)&&(f.setValue(g.items),delete g.items),D&&w.on("invalid"+m,function(e){e.preventDefault(),f.isInvalid=!0,f.refreshState()}),f.updateOriginalInput(),f.refreshItems(),f.refreshState(),f.updatePlaceholder(),f.isSetup=!0,w.is(":disabled")&&f.disable(),f.on("change",this.onChange),w.data("selectize",f),w.addClass("selectized"),f.trigger("initialize"),!0===g.preload&&f.onSearchChange("")},setupTemplates:function(){var n=this.settings.labelField,r=this.settings.optgroupLabelField,e={optgroup:function(e){return'
    '+e.html+"
    "},optgroup_header:function(e,t){return'
    '+t(e[r])+"
    "},option:function(e,t){return'
    '+t(e[n])+"
    "},item:function(e,t){return'
    '+t(e[n])+"
    "},option_create:function(e,t){return'
    Add '+t(e.input)+"
    "}};this.settings.render=S.extend({},e,this.settings.render)},setupCallbacks:function(){var e,t,n={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(e in n)n.hasOwnProperty(e)&&(t=this.settings[n[e]])&&this.on(e,t)},onClick:function(e){this.isFocused&&this.isOpen||(this.focus(),e.preventDefault())},onMouseDown:function(e){var t=this,n=e.isDefaultPrevented();S(e.target);if(t.isFocused){if(e.target!==t.$control_input[0])return"single"===t.settings.mode?t.isOpen?t.close():t.open():n||t.setActiveItem(null),!1}else n||window.setTimeout(function(){t.focus()},0)},onChange:function(){this.$input.trigger("change")},onPaste:function(e){var i=this;i.isFull()||i.isInputHidden||i.isLocked?e.preventDefault():i.settings.splitOn&&setTimeout(function(){var e=i.$control_input.val();if(e.match(i.settings.splitOn))for(var t=S.trim(e).split(i.settings.splitOn),n=0,r=t.length;n=this.settings.maxItems},updateOriginalInput:function(e){var t,n,r,i,o=this;if(e=e||{},1===o.tagType){for(r=[],t=0,n=o.items.length;t'+s(i)+"");r.length||this.$input.attr("multiple")||r.push(''),o.$input.html(r.join(""))}else o.$input.val(o.getValue()),o.$input.attr("value",o.$input.val());o.isSetup&&(e.silent||o.trigger("change",o.$input.val()))},updatePlaceholder:function(){if(this.settings.placeholder){var e=this.$control_input;this.items.length?e.removeAttr("placeholder"):e.attr("placeholder",this.settings.placeholder),e.triggerHandler("update",{force:!0})}},open:function(){var e=this;e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.focus(),e.isOpen=!0,e.refreshState(),e.$dropdown.css({visibility:"hidden",display:"block"}),e.positionDropdown(),e.$dropdown.css({visibility:"visible"}),e.trigger("dropdown_open",e.$dropdown))},close:function(){var e=this,t=e.isOpen;"single"===e.settings.mode&&e.items.length&&(e.hideInput(),e.isBlurring||e.$control_input.blur()),e.isOpen=!1,e.$dropdown.hide(),e.setActiveOption(null),e.refreshState(),t&&e.trigger("dropdown_close",e.$dropdown)},positionDropdown:function(){var e=this.$control,t="body"===this.settings.dropdownParent?e.offset():e.position();t.top+=e.outerHeight(!0),this.$dropdown.css({width:e[0].getBoundingClientRect().width,top:t.top,left:t.left})},clear:function(e){var t=this;t.items.length&&(t.$control.children(":not(input)").remove(),t.items=[],t.lastQuery=null,t.setCaret(0),t.setActiveItem(null),t.updatePlaceholder(),t.updateOriginalInput({silent:e}),t.refreshState(),t.showInput(),t.trigger("clear"))},insertAtCaret:function(e){var t=Math.min(this.caretPos,this.items.length),n=e[0],r=this.buffer||this.$control[0];0===t?r.insertBefore(n,r.firstChild):r.insertBefore(n,r.childNodes[t]),this.setCaret(t+1)},deleteSelection:function(e){var t,n,r,i,o,a,s,l,u,c=this;if(r=e&&8===e.keyCode?-1:1,i=f(c.$control_input[0]),c.$activeOption&&!c.settings.hideSelected&&(s=c.getAdjacentOption(c.$activeOption,-1).attr("data-value")),o=[],c.$activeItems.length){for(u=c.$control.children(".active:"+(0
    '+e.title+'×
    '}},e),n.setup=(t=n.setup,function(){t.apply(n,arguments),n.$dropdown_header=S(e.html(e)),n.$dropdown.prepend(n.$dropdown_header)})}),w.define("optgroup_columns",function(s){var o,l=this;s=S.extend({equalizeWidth:!0,equalizeHeight:!0},s),this.getAdjacentOption=function(e,t){var n=e.closest("[data-group]").find("[data-selectable]"),r=n.index(e)+t;return 0<=r&&r
    ',e=e.firstChild,n.body.appendChild(e),t=u.width=e.offsetWidth-e.clientWidth,n.body.removeChild(e)),t},e=function(){var e,t,n,r,i,o,a;if((t=(a=S("[data-group]",l.$dropdown_content)).length)&&l.$dropdown_content.width()){if(s.equalizeHeight){for(e=n=0;e'+t.label+"",o.setup=(n=r.setup,function(){if(t.append){var i=r.settings.render.item;r.settings.render.item=function(e){return t=i.apply(o,arguments),n=a,r=t.search(/(<\/[^>]+>\s*)$/),t.substring(0,r)+n+t.substring(r);var t,n,r}}n.apply(o,arguments),o.$control.on("click","."+t.className,function(e){if(e.preventDefault(),!r.isLocked){var t=S(e.currentTarget).parent();r.setActiveItem(t),r.deleteSelection()&&r.setCaret(r.items.length)}})})):function(i,t){t.className="remove-single";var n,o=i,a=''+t.label+"";i.setup=(n=o.setup,function(){if(t.append){var e=S(o.$input.context).attr("id"),r=(S("#"+e),o.settings.render.item);o.settings.render.item=function(e){return t=r.apply(i,arguments),n=a,S("").append(t).append(n);var t,n}}n.apply(i,arguments),i.$control.on("click","."+t.className,function(e){e.preventDefault(),o.isLocked||o.clear()})})}(this,e)}),w.define("restore_on_backspace",function(r){var i,e=this;r.text=r.text||function(e){return e[this.settings.labelField]},this.onKeyDown=(i=e.onKeyDown,function(e){var t,n;return 8===e.keyCode&&""===this.$control_input.val()&&!this.$activeItems.length&&0<=(t=this.caretPos-1)&&t",{class:function(){var e=[];return e.push(i.options.state?"on":"off"),i.options.size&&e.push(i.options.size),i.options.disabled&&e.push("disabled"),i.options.readonly&&e.push("readonly"),i.options.indeterminate&&e.push("indeterminate"),i.options.inverse&&e.push("inverse"),i.$element.attr("id")&&e.push("id-"+i.$element.attr("id")),e.map(i._getClass.bind(i)).concat([i.options.baseClass],i._getClasses(i.options.wrapperClass)).join(" ")}}),this.$container=s("
    ",{class:this._getClass("container")}),this.$on=s("",{html:this.options.onText,class:this._getClass("handle-on")+" "+this._getClass(this.options.onColor)}),this.$off=s("",{html:this.options.offText,class:this._getClass("handle-off")+" "+this._getClass(this.options.offColor)}),this.$label=s("",{html:this.options.labelText,class:this._getClass("label")}),this.$element.on("init.bootstrapSwitch",this.options.onInit.bind(this,r)),this.$element.on("switchChange.bootstrapSwitch",function(){for(var e=arguments.length,t=Array(e),n=0;n-n._handleWidth/2;n._dragEnd=!1,n.state(n.options.inverse?!t:t)}else n.state(!n.options.state);n._dragStart=!1}},"mouseleave.bootstrapSwitch":function(){n.$label.trigger("mouseup.bootstrapSwitch")}})}},{key:"_externalLabelHandler",value:function(){var t=this,n=this.$element.closest("label");n.on("click",function(e){e.preventDefault(),e.stopImmediatePropagation(),e.target===n[0]&&t.toggleState()})}},{key:"_formHandler",value:function(){var e=this.$element.closest("form");e.data("bootstrap-switch")||e.on("reset.bootstrapSwitch",function(){window.setTimeout(function(){e.find("input").filter(function(){return s(this).data("bootstrap-switch")}).each(function(){return s(this).bootstrapSwitch("state",this.checked)})},1)}).data("bootstrap-switch",!0)}},{key:"_getClass",value:function(e){return this.options.baseClass+"-"+e}},{key:"_getClasses",value:function(e){return s.isArray(e)?e.map(this._getClass.bind(this)):[this._getClass(e)]}}]),t}();s.fn.bootstrapSwitch=function(o){for(var e=arguments.length,a=Array(1
    ');var r,i=f.overlay?"":" ngdialog-no-overlay";if((d=T('
    ')).html(f.overlay?'
    '+t+"
    ":'
    '+t+"
    "),d.data("$ngDialogOptions",f),c.ngDialogId=u,f.data&&O.isString(f.data)){var o=f.data.replace(/^\s*/,"")[0];c.ngDialogData="{"===o||"["===o?O.fromJson(f.data):new String(f.data),c.ngDialogData.ngDialogId=u}else f.data&&O.isObject(f.data)&&(c.ngDialogData=f.data,c.ngDialogData.ngDialogId=u);(f.className&&d.addClass(f.className),f.appendClassName&&d.addClass(f.appendClassName),f.width&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.width)?h.style.width=f.width:h.style.width=f.width+"px"),f.height&&(h=d[0].querySelector(".ngdialog-content"),O.isString(f.height)?h.style.height=f.height:h.style.height=f.height+"px"),f.disableAnimation&&d.addClass("ngdialog-disabled-animation"),p=f.appendTo&&O.isString(f.appendTo)?O.element(document.querySelector(f.appendTo)):b.body,$.applyAriaAttributes(d,f),f.preCloseCallback)&&(O.isFunction(f.preCloseCallback)?r=f.preCloseCallback:O.isString(f.preCloseCallback)&&c&&(O.isFunction(c[f.preCloseCallback])?r=c[f.preCloseCallback]:c.$parent&&O.isFunction(c.$parent[f.preCloseCallback])?r=c.$parent[f.preCloseCallback]:m&&O.isFunction(m[f.preCloseCallback])&&(r=m[f.preCloseCallback])),r&&d.data("$ngDialogPreCloseCallback",r));if(c.closeThisDialog=function(e){$.closeDialog(d,e)},f.controller&&(O.isString(f.controller)||O.isArray(f.controller)||O.isFunction(f.controller))){var a;f.controllerAs&&O.isString(f.controllerAs)&&(a=f.controllerAs);var s=w(f.controller,O.extend(n,{$scope:c,$element:d}),!0,a);f.bindToController&&O.extend(s.instance,{ngDialogId:c.ngDialogId,ngDialogData:c.ngDialogData,closeThisDialog:c.closeThisDialog,confirm:c.confirm}),"function"==typeof s?d.data("$ngDialogControllerController",s()):d.data("$ngDialogControllerController",s)}if(v(function(){var e=document.querySelectorAll(".ngdialog");$.deactivateAll(e),g(d)(c);var t=y.innerWidth-b.body.prop("clientWidth");b.html.addClass(f.bodyClassName),b.body.addClass(f.bodyClassName);var n=t-(y.innerWidth-b.body.prop("clientWidth"));0window.innerHeight&&(l=v,t++,e=0);var u=l?0===e?l:l+w:v,c=n+t*(b+s);o.css(o._positionY,u+"px"),"center"==o._positionX?o.css("left",parseInt(window.innerWidth/2-s/2)+"px"):o.css(o._positionX,c+"px"),r[o._positionY+o._positionX]=u+a,0m.maxCount&&0===i&&o.scope().kill(!0),e++}}},i=c(e)(n);i._positionY=h.positionY,i._positionX=h.positionX,i._priority=h.priority,i.addClass(h.type);var o=function(e){("click"===(e=e.originalEvent||e).type||"opacity"===e.propertyName&&1<=e.elapsedTime)&&(n.onClose&&n.$apply(n.onClose(i)),i.remove(),$.splice($.indexOf(i),1),n.$destroy(),r())};h.closeOnClick&&(i.addClass("clickable"),i.bind("click",o)),i.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",o),angular.isNumber(h.delay)&&u(function(){i.addClass("killed")},h.delay),t("none"),angular.element(document.querySelector(h.container)).append(i);var a=-(parseInt(i[0].offsetHeight)+50);if(i.css(i._positionY,a+"px"),$.push(i),"center"==h.positionX){var s=parseInt(i[0].offsetWidth);i.css("left",parseInt(window.innerWidth/2-s/2)+"px")}u(function(){t("")}),n._templateElement=i,n.kill=function(e){e?(n.onClose&&n.$apply(n.onClose(n._templateElement)),$.splice($.indexOf(n._templateElement),1),n._templateElement.remove(),n.$destroy(),u(r)):n._templateElement.addClass("killed")},u(r),_||(angular.element(g).bind("resize",function(e){u(r)}),_=!0),l.resolve(n)}var l=a.defer();"object"==typeof h&&null!==h||(h={message:h}),h.scope=h.scope?h.scope:o,h.template=h.templateUrl?h.templateUrl:m.templateUrl,h.delay=angular.isUndefined(h.delay)?s:h.delay,h.type=e||h.type||m.type||"",h.positionY=h.positionY?h.positionY:m.positionY,h.positionX=h.positionX?h.positionX:m.positionX,h.replaceMessage=h.replaceMessage?h.replaceMessage:m.replaceMessage,h.onClose=h.onClose?h.onClose:m.onClose,h.closeOnClick=null!==h.closeOnClick&&void 0!==h.closeOnClick?h.closeOnClick:m.closeOnClick,h.container=h.container?h.container:m.container,h.priority=h.priority?h.priority:m.priority;var n=i.get(h.template);return n?t(n):r.get(h.template,{cache:!0}).then(function(e){t(e.data)}).catch(function(e){throw new Error("Template ("+h.template+") could not be loaded. "+e)}),l.promise};return t.primary=function(e){return this(e,"primary")},t.error=function(e){return this(e,"error")},t.success=function(e){return this(e,"success")},t.info=function(e){return this(e,"info")},t.warning=function(e){return this(e,"warning")},t.clearAll=function(){angular.forEach($,function(e){e.addClass("killed")})},t}]}),angular.module("ui-notification").run(["$templateCache",function(e){e.put("angular-ui-notification.html",'

    ')}]),function(){var w="__default";angular.module("angularUtils.directives.dirPagination",[]).directive("dirPaginate",["$compile","$parse","paginationService",function(m,v,y){return{terminal:!0,multiElement:!0,priority:100,compile:function(e,t){var f=t.dirPaginate,n=f.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),r=/\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;if(null===n[2].match(r))throw"pagination directive: the 'itemsPerPage' filter must be set.";var i=n[2].replace(r,""),g=v(i);o=e,angular.forEach(o,function(e){1===e.nodeType&&angular.element(e).attr("dir-paginate-no-compile",!0)});var o;var a=t.paginationId||w;return y.registerInstance(a),function(e,t,n){var r=v(n.paginationId)(e)||n.paginationId||w;y.registerInstance(r);var i,o,a,s,l,u,c,d=(u=r,c=!!(l=f).match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/),u===w||c?l:l.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/,"$1 : '"+u+"'"));o=n,a=d,(i=t)[0].hasAttribute("dir-paginate-start")||i[0].hasAttribute("data-dir-paginate-start")?(o.$set("ngRepeatStart",a),i.eq(i.length-1).attr("ng-repeat-end",!0)):o.$set("ngRepeat",a),s=t,angular.forEach(s,function(e){1===e.nodeType&&angular.element(e).removeAttr("dir-paginate-no-compile")}),s.eq(0).removeAttr("dir-paginate-start").removeAttr("dir-paginate").removeAttr("data-dir-paginate-start").removeAttr("data-dir-paginate"),s.eq(s.length-1).removeAttr("dir-paginate-end").removeAttr("data-dir-paginate-end");var p=m(t),h=function(e,t,n){var r;if(t.currentPage)r=v(t.currentPage);else{var i=(n+"__currentPage").replace(/\W/g,"_");e[i]=1,r=v(i)}return r}(e,n,r);y.setCurrentPageParser(r,h,e),void 0!==n.totalItems?(y.setAsyncModeTrue(r),e.$watch(function(){return v(n.totalItems)(e)},function(e){0<=e&&y.setCollectionLength(r,e)})):(y.setAsyncModeFalse(r),e.$watchCollection(function(){return g(e)},function(e){if(e){var t=e instanceof Array?e.length:Object.keys(e).length;y.setCollectionLength(r,t)}})),p(e)}}}}]).directive("dirPaginateNoCompile",function(){return{priority:5e3,terminal:!0}}).directive("dirPaginationControls",["paginationService","paginationTemplate",function(d,n){var p=/^\d+$/,e={restrict:"AE",scope:{maxSize:"=?",onPageChange:"&?",paginationId:"=?",autoHide:"=?"},link:function(r,e,t){var n=t.paginationId||w,i=r.paginationId||t.paginationId||w;if(!d.isRegistered(i)&&!d.isRegistered(n)){var o=i!==w?" (id: "+i+") ":" ";window.console&&console.warn("Pagination directive: the pagination controls"+o+"cannot be used without the corresponding pagination directive, which was not found at link time.")}r.maxSize||(r.maxSize=9);r.autoHide=void 0===r.autoHide||r.autoHide,r.directionLinks=!angular.isDefined(t.directionLinks)||r.$parent.$eval(t.directionLinks),r.boundaryLinks=!!angular.isDefined(t.boundaryLinks)&&r.$parent.$eval(t.boundaryLinks);var a=Math.max(r.maxSize,5);function s(e){if(d.isRegistered(i)&&c(e)){var t=r.pagination.current;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,u(),r.onPageChange&&r.onPageChange({newPageNumber:e,oldPageNumber:t})}}function l(){if(d.isRegistered(i)){var e=parseInt(d.getCurrentPage(i))||1;r.pages=h(e,d.getCollectionLength(i),d.getItemsPerPage(i),a),r.pagination.current=e,r.pagination.last=r.pages[r.pages.length-1],r.pagination.last
  • «
  • {{ pageNumber }}
  • »
  • ')}])}();var com_github_culmat_jsTreeTable=function(){function l(e,r,i){return i=i||"children",$.each(e,function(e,t){!function n(e){e[i]&&$.each(e[i],function(e,t){n(t)}),r(e)}(t)}),e}function t(e,n,o,a){var t=e;n=n||"id",o=o||"parent",a=a||"children";var s=[];$.each(t,function(e,t){s[t[n]]=t});var l=[];return $.each(t,function(e,r){var t=r[o];if($.isArray(t)||(t=[t]),0==t.length)l.push(r);else{var i=!1;$.each(t,function(e,t){var n=s[t];n&&(n[a]||(n[a]=[]),$.inArray(r,n[a])<0&&n[a].push(r),i=!0)}),i||l.push(r)}}),l}function u(e,u,c,d,p,t){u=u||"children",c=c||"id",t=t||{};var n=0,r=$("");$.each(t,function(e,t){"class"==e&&"jsTT"!=t?r.addClass(t):r.attr(e,t)});var i=$(""),o=$(""),h=$("");return r.append(i),i.append(o),r.append(h),d?$.each(d,function(e,t){$(o).append($(""))}):($(o).append($("")),$.each(e[0],function(e,t){e!=u&&e!=c&&$(o).append($(""))})),function o(e,a,s,l){n=Math.max(n,s),$.each(e,function(e,n){var r,t,i;n["data-tt-level"]=s,r=n,t=l,i=$(""),$(i).attr("data-tt-id",r[c]),$(i).attr("data-tt-level",r["data-tt-level"]),r[u]&&0!=r[u].length?$(i).attr("data-tt-isnode",!0):$(i).attr("data-tt-isleaf",!0),t&&$(i).attr("data-tt-parent-id",t[c]),p?p($(i),r):d?$.each(d,function(e,t){$(i).append($(""))}):($(i).append($("")),$.each(r,function(e,t){e!=u&&e!=c&&"data-tt-level"!=e&&$(i).append($(""))})),h.append(i),n[a]&&$.each(n[a],function(e,t){o([t],a,s+1,n)})})}(e,u,1),e[0]&&(e[0].maxLevel=n),r}function n(e,t){return $.each(e,function(e,n){$.each(t,function(e,t){n[t]=$(n).attr(t)})}),e}function c(i){i.addClass("jsTT"),i.expandLevel=function(n){$("tr[data-tt-level]",i).each(function(e){var t=parseInt($(this).attr("data-tt-level"));n-1')):r.prepend($('')),r.prepend($('')),r.css("white-space","nowrap"),t.trExpand=function(e){if(!(this.trChildren.length<1)){e&&(this.trChildrenVisible=!0,$("#state",this).get(0).src=o);var n=e||this.trChildrenVisible;$.each(this.trChildren,function(e,t){n&&$(t).css("display","table-row"),t.trExpand()})}},t.trCollapse=function(e){this.trChildren.length<1||(e&&(this.trChildrenVisible=!1,$("#state",this).get(0).src="data:image/png;base64,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"),$.each(this.trChildren,function(e,t){$(t).css("display","none"),t.trCollapse()}))},$(t).click(function(){this.trChildrenVisible?this.trCollapse(!0):this.trExpand(!0)})}),i}return{depthFirst:l,makeTree:t,renderTree:u,attr2attr:n,treeTable:c,appendTreetable:function(e,t){(t=t||{}).idAttr=t.idAttr||"id",t.childrenAttr=t.childrenAttr||"children";var n=t.controls||[];t.mountPoint||(t.mountPoint=$("body")),t.depthFirst&&l(e,t.depthFirst,t.childrenAttr);var r=u(e,t.childrenAttr,t.idAttr,t.renderedAttr,t.renderer,t.tableAttributes);c(r),t.replaceContent&&t.mountPoint.html("");var i,o,a=t.initialExpandLevel?parseInt(t.initialExpandLevel):-1;if(a=Math.min(a,e[0].maxLevel),r.expandLevel(a),t.slider){var s=$('
    ');s.width("200px"),s.slider({min:1,max:e[0].maxLevel,range:"min",value:a,slide:function(e,t){r.expandLevel(t.value)}}),n=[s].concat(t.controls)}return 0"),$.each(i,function(e,t){o.append($('
    "+t+""+c+""+e+"
    "+r[e]+""+r[c]+""+t+"').append(t))}),$('').append(o))),t.mountPoint.append(r),r},jsTreeTable:"1.0",register:function(n){$.each(this,function(e,t){"register"!=e&&(n[e]=t)})}}}(); \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/gulpfile.js b/sentinel-dashboard/src/main/webapp/resources/gulpfile.js new file mode 100755 index 00000000..f4c1e4cf --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/gulpfile.js @@ -0,0 +1,126 @@ +const gulp = require('gulp'); +const plugins = require('gulp-load-plugins')(); +const open = require('open'); +const app = { + srcPath: 'app/', // 源代码 + devPath: 'tmp/', // 开发打包 + prdPath: 'dist/' // 生产打包 +}; + +const JS_LIBS = [ + 'node_modules/angular-ui-router/release/angular-ui-router.js', + 'node_modules/oclazyload/dist/ocLazyLoad.min.js', + 'node_modules/angular-loading-bar/build/loading-bar.min.js', + 'node_modules/angular-bootstrap/ui-bootstrap-tpls.min.js', + 'node_modules/moment/moment.js', + 'node_modules/angular-date-time-input/src/dateTimeInput.js', + 'node_modules/angularjs-bootstrap-datetimepicker/src/js/datetimepicker.js', + 'node_modules/angular-table-resize/dist/angular-table-resize.min.js', + 'node_modules/angular-clipboard/angular-clipboard.js', + 'node_modules/selectize/dist/js/standalone/selectize.js', + 'node_modules/angular-selectize2/dist/selectize.js', + 'node_modules/bootstrap-switch/dist/js/bootstrap-switch.min.js', + 'node_modules/ng-dialog/js/ngDialog.js', + 'node_modules/angular-ui-notification/dist/angular-ui-notification.min.js', + 'node_modules/angular-utils-pagination/dirPagination.js', + 'app/scripts/libs/treeTable.js', +]; + +const CSS_APP = [ + 'node_modules/angular-loading-bar/build/loading-bar.min.css', + 'node_modules/bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css', + 'node_modules/ng-dialog/css/ngDialog.min.css', + 'node_modules/ng-dialog/css/ngDialog-theme-default.css', + 'node_modules/angularjs-bootstrap-datetimepicker/src/css/datetimepicker.css', + 'node_modules/angular-ui-notification/dist/angular-ui-notification.min.css', + 'node_modules/angular-table-resize/dist/angular-table-resize.css', + 'node_modules/selectize/dist/css/selectize.css', + 'app/styles/page.css', + 'app/styles/timeline.css', + 'app/styles/main.css' +]; + +const JS_APP = [ + 'app/scripts/app.js', + 'app/scripts/filters/filters.js', + 'app/scripts/services/appservice.js', + 'app/scripts/services/flowservice.js', + 'app/scripts/services/degradeservice.js', + 'app/scripts/services/systemservice.js', + 'app/scripts/services/machineservice.js', + 'app/scripts/services/identityservice.js', + 'app/scripts/services/metricservice.js', +]; + +gulp.task('lib', function () { + gulp.src(JS_LIBS) + .pipe(plugins.concat('app.vendor.js')) + .pipe(gulp.dest(app.devPath + 'js')) + .pipe(plugins.uglify()) + .pipe(gulp.dest(app.prdPath + 'js')) + .pipe(plugins.connect.reload()); +}); + +/* +* css任务 +* 在src下创建style文件夹,里面存放less文件。 +*/ +gulp.task('css', function () { + gulp.src(CSS_APP) + .pipe(plugins.concat('app.css')) + .pipe(gulp.dest(app.devPath + 'css')) + .pipe(plugins.cssmin()) + .pipe(gulp.dest(app.prdPath + 'css')) + .pipe(plugins.connect.reload()); +}); + +/* +* js任务 +* 在src目录下创建script文件夹,里面存放所有的js文件 +*/ +gulp.task('js', function () { + gulp.src(JS_APP) + .pipe(plugins.concat('app.js')) + .pipe(gulp.dest(app.devPath + 'js')) + .pipe(plugins.uglify()) + .pipe(gulp.dest(app.prdPath + 'js')) + .pipe(plugins.connect.reload()); +}); + +/* +* js任务 +* 在src目录下创建script文件夹,里面存放所有的js文件 +*/ +gulp.task('jshint', function () { + gulp.src(JS_APP) + .pipe(plugins.jshint()) + .pipe(plugins.jshint.reporter()); +}); + +// 每次发布的时候,可能需要把之前目录内的内容清除,避免旧的文件对新的容有所影响。 需要在每次发布前删除dist和build目录 +gulp.task('clean', function () { + gulp.src([app.devPath, app.prdPath]) + .pipe(plugins.clean()); +}); + +// 总任务 +gulp.task('build', ['clean', 'jshint', 'lib', 'js', 'css']); + +// 服务 +gulp.task('serve', ['build'], function () { + plugins.connect.server({ //启动一个服务器 + root: [app.devPath], // 服务器从哪个路径开始读取,默认从开发路径读取 + livereload: true, // 自动刷新 + port: 1234 + }); + // 打开浏览器 + setTimeout(() => { + open('http://localhost:8080/index_dev.htm') + }, 200); + // 监听 + gulp.watch(app.srcPath + '**/*.js', ['js']); + gulp.watch(app.srcPath + '**/*.css', ['css']); +}); + +// 定义default任务 +gulp.task('default', ['serve']); diff --git a/sentinel-dashboard/src/main/webapp/resources/index.htm b/sentinel-dashboard/src/main/webapp/resources/index.htm new file mode 100755 index 00000000..46032f5c --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/index.htm @@ -0,0 +1,28 @@ + + + + + + Sentinel 控制台 + + + + + + + + + +
    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/index_dev.htm b/sentinel-dashboard/src/main/webapp/resources/index_dev.htm new file mode 100755 index 00000000..d909226f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/index_dev.htm @@ -0,0 +1,28 @@ + + + + + + Sentinel 控制台 + + + + + + + + + +
    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/license-stat.csv b/sentinel-dashboard/src/main/webapp/resources/license-stat.csv new file mode 100755 index 00000000..fd3c5caa --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/license-stat.csv @@ -0,0 +1,26 @@ +Type,Package,License +npm,angular,MIT License +npm,angular-animate,MIT License +npm,angular-bootstrap,MIT License +npm,angular-clipboard,MIT License +npm,angular-cookies,MIT License +npm,angular-date-time-input,MIT License +npm,angular-loading-bar,MIT License +npm,angular-mocks,MIT License +npm,angular-resource,MIT License +npm,angular-route,MIT License +npm,angular-selectize2,MIT License +npm,angular-table-resize,MIT License +npm,angular-touch,MIT License +npm,angular-ui-notification,MIT License +npm,angular-ui-router,MIT License +npm,angular-utils-pagination,MIT License +npm,angularjs-bootstrap-datetimepicker,MIT License +npm,bootstrap-switch,Apache License 2.0 +npm,bootstrap-tagsinput,MIT License +npm,moment,MIT License +npm,ng-dialog,MIT License +npm,ng-tags-input,MIT License +npm,oclazyload,MIT License +npm,selectize,Apache License 2.0 +lib,jsTreeTable,MIT License \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/package-lock.json b/sentinel-dashboard/src/main/webapp/resources/package-lock.json new file mode 100644 index 00000000..7976db2a --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/package-lock.json @@ -0,0 +1,5015 @@ +{ + "name": "sentinel-dashboard", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@uirouter/core": { + "version": "5.0.20", + "resolved": "http://registry.npm.alibaba-inc.com/@uirouter/core/download/@uirouter/core-5.0.20.tgz", + "integrity": "sha1-nCfY0U1QNYBAhA/QDp04w2vhrcU=" + }, + "accepts": { + "version": "1.3.5", + "resolved": "http://registry.npm.alibaba-inc.com/accepts/download/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dev": true, + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/amdefine/download/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "angular": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular/download/angular-1.7.2.tgz", + "integrity": "sha1-aHuVXb5cUz+Nc0YEYXB68ANgJR8=" + }, + "angular-animate": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-animate/download/angular-animate-1.7.2.tgz", + "integrity": "sha1-SNHSLK8AV7V7UPiCto1Rwpeqd0c=" + }, + "angular-bootstrap": { + "version": "0.12.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-bootstrap/download/angular-bootstrap-0.12.2.tgz", + "integrity": "sha1-3pkAp9sIen6dpHMEqcL3f5wGmCc=" + }, + "angular-clipboard": { + "version": "1.6.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-clipboard/download/angular-clipboard-1.6.2.tgz", + "integrity": "sha1-RwjlodyU85QKuJhh6h4ZsmdUFU8=" + }, + "angular-cookies": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-cookies/download/angular-cookies-1.7.2.tgz", + "integrity": "sha1-y6OpGB0qSMLYGV/3/jKQVfupcLo=" + }, + "angular-date-time-input": { + "version": "1.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/angular-date-time-input/download/angular-date-time-input-1.2.1.tgz", + "integrity": "sha1-bXt+tH+yz2GYylZMXdW8UVYnnXY=", + "requires": { + "angular": "^1.x", + "moment": "^2.15.x" + } + }, + "angular-loading-bar": { + "version": "0.9.0", + "resolved": "http://registry.npm.alibaba-inc.com/angular-loading-bar/download/angular-loading-bar-0.9.0.tgz", + "integrity": "sha1-N+9Swl8QLCFuezzf0vxaXflijkU=" + }, + "angular-mocks": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-mocks/download/angular-mocks-1.7.2.tgz", + "integrity": "sha1-iivHp841VpekirAOVIuI549x+lI=" + }, + "angular-resource": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-resource/download/angular-resource-1.7.2.tgz", + "integrity": "sha1-NtHd9AaCuY96lYA9Y6ZMxM5Wcqg=" + }, + "angular-route": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-route/download/angular-route-1.7.2.tgz", + "integrity": "sha1-okt6DI7wmj1OLKIN5/hODoV6Ya4=" + }, + "angular-selectize2": { + "version": "1.2.3", + "resolved": "http://registry.npm.alibaba-inc.com/angular-selectize2/download/angular-selectize2-1.2.3.tgz", + "integrity": "sha1-rJJvgtckbYcBC6W+N/BKrfkRbZU=" + }, + "angular-table-resize": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/angular-table-resize/download/angular-table-resize-2.0.1.tgz", + "integrity": "sha1-yZaAqAOA9SEu/HHRTfk3uTdOmOk=", + "requires": { + "angular": "^1.5.7", + "jquery": "^3.0.0" + } + }, + "angular-touch": { + "version": "1.7.2", + "resolved": "http://registry.npm.alibaba-inc.com/angular-touch/download/angular-touch-1.7.2.tgz", + "integrity": "sha1-VLoh5nUgsPv2ocenz6RfvY/Fnvc=" + }, + "angular-ui-notification": { + "version": "0.3.6", + "resolved": "http://registry.npm.alibaba-inc.com/angular-ui-notification/download/angular-ui-notification-0.3.6.tgz", + "integrity": "sha1-MzvL6ComUUgrOcNZWzy/qI4wvPc=" + }, + "angular-ui-router": { + "version": "1.0.19", + "resolved": "http://registry.npm.alibaba-inc.com/angular-ui-router/download/angular-ui-router-1.0.19.tgz", + "integrity": "sha1-nB9KVMimX3xrcKm00IGPXxL7AaE=", + "requires": { + "@uirouter/core": "5.0.20" + } + }, + "angular-utils-pagination": { + "version": "0.11.1", + "resolved": "http://registry.npm.alibaba-inc.com/angular-utils-pagination/download/angular-utils-pagination-0.11.1.tgz", + "integrity": "sha1-7618iHm+swrT13cH+T49DvUfLGY=" + }, + "angularjs-bootstrap-datetimepicker": { + "version": "1.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/angularjs-bootstrap-datetimepicker/download/angularjs-bootstrap-datetimepicker-1.1.4.tgz", + "integrity": "sha1-iKT+oORv6PRWHirDKWT/WJ5UZKk=", + "requires": { + "angular": "^1.6.4", + "moment": "^2.18.1" + } + }, + "ansi-colors": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-colors/download/ansi-colors-1.1.0.tgz", + "integrity": "sha1-Y3S03V1HGP884npnGjscrQdxMqk=", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-cyan/download/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-gray/download/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-red/download/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-regex/download/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-styles/download/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-wrap/download/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "ansicolors": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansicolors/download/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + }, + "archy": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/archy/download/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/arr-diff/download/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/arr-flatten/download/arr-flatten-1.1.0.tgz", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/arr-union/download/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/array-differ/download/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/array-each/download/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/array-find-index/download/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-slice": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/array-slice/download/array-slice-1.1.0.tgz", + "integrity": "sha1-42jqFfibxwaff/uJrsOmx9SsItQ=", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/array-uniq/download/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/array-unique/download/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/assign-symbols/download/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "2.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/async/download/async-2.6.1.tgz", + "integrity": "sha1-skWiPKcZMAROxT+kaqAKPofGphA=", + "requires": { + "lodash": "^4.17.10" + } + }, + "atob": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/atob/download/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/balanced-match/download/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "http://registry.npm.alibaba-inc.com/base/download/base-0.11.2.tgz", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/is-descriptor/download/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "batch": { + "version": "0.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/batch/download/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "beeper": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/beeper/download/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, + "body-parser": { + "version": "1.14.2", + "resolved": "http://registry.npm.alibaba-inc.com/body-parser/download/body-parser-1.14.2.tgz", + "integrity": "sha1-EBXLH+LEQ4WCWVgdtTMy+NDPUPk=", + "dev": true, + "requires": { + "bytes": "2.2.0", + "content-type": "~1.0.1", + "debug": "~2.2.0", + "depd": "~1.1.0", + "http-errors": "~1.3.1", + "iconv-lite": "0.4.13", + "on-finished": "~2.3.0", + "qs": "5.2.0", + "raw-body": "~2.1.5", + "type-is": "~1.6.10" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/debug/download/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "http://registry.npm.alibaba-inc.com/ms/download/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "qs": { + "version": "5.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/qs/download/qs-5.2.0.tgz", + "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=", + "dev": true + } + } + }, + "bootstrap-switch": { + "version": "3.3.4", + "resolved": "http://registry.npm.alibaba-inc.com/bootstrap-switch/download/bootstrap-switch-3.3.4.tgz", + "integrity": "sha1-cOCusqh3wNx2aZHeEI4hcPwpov8=" + }, + "bootstrap-tagsinput": { + "version": "0.7.1", + "resolved": "http://registry.npm.alibaba-inc.com/bootstrap-tagsinput/download/bootstrap-tagsinput-0.7.1.tgz", + "integrity": "sha1-/+Owa74qEGlF7ygUVoAFqU8hGTc=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "http://registry.npm.alibaba-inc.com/brace-expansion/download/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/braces/download/braces-2.3.2.tgz", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/builtin-modules/download/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "bytes": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/bytes/download/bytes-2.2.0.tgz", + "integrity": "sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/cache-base/download/cache-base-1.0.1.tgz", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/camelcase/download/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/camelcase-keys/download/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "cardinal": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/cardinal/download/cardinal-1.0.0.tgz", + "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "requires": { + "ansicolors": "~0.2.1", + "redeyed": "~1.0.0" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npm.alibaba-inc.com/chalk/download/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "http://registry.npm.alibaba-inc.com/class-utils/download/class-utils-0.3.6.tgz", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "3.4.28", + "resolved": "http://registry.npm.alibaba-inc.com/clean-css/download/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "dev": true, + "requires": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "http://registry.npm.alibaba-inc.com/commander/download/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "cli": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/cli/download/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/clone/download/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/clone-buffer/download/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/clone-stats/download/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/cloneable-readable/download/cloneable-readable-1.1.2.tgz", + "integrity": "sha1-1ZHe5Kj4vBXaQ86X3O66E9Q+KmU=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/string_decoder/download/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/collection-visit/download/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-support": { + "version": "1.1.3", + "resolved": "http://registry.npm.alibaba-inc.com/color-support/download/color-support-1.1.3.tgz", + "integrity": "sha1-k4NDeaHMmgxh+C9S8NBDIiUb1aI=", + "dev": true + }, + "commander": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/commander/download/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/component-emitter/download/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/concat-map/download/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/concat-with-sourcemaps/download/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha1-1OqT8FriV5CVG5nns7CeOQikCC4=", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "dev": true + } + } + }, + "connect": { + "version": "3.6.6", + "resolved": "http://registry.npm.alibaba-inc.com/connect/download/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + } + }, + "connect-livereload": { + "version": "0.5.4", + "resolved": "http://registry.npm.alibaba-inc.com/connect-livereload/download/connect-livereload-0.5.4.tgz", + "integrity": "sha1-gBV9E3HJ83zBQDmrGJWXDRGdw7w=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/console-browserify/download/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/content-type/download/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/copy-descriptor/download/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/core-util-is/download/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "csscomb": { + "version": "3.1.8", + "resolved": "http://registry.npm.alibaba-inc.com/csscomb/download/csscomb-3.1.8.tgz", + "integrity": "sha1-qKc4iE9Am6817JRhr8UuHHW9I6I=", + "dev": true, + "requires": { + "commander": "2.0.0", + "csscomb-core": "3.0.0-3.1", + "gonzales-pe": "3.0.0-28", + "vow": "0.4.4" + } + }, + "csscomb-core": { + "version": "3.0.0-3.1", + "resolved": "http://registry.npm.alibaba-inc.com/csscomb-core/download/csscomb-core-3.0.0-3.1.tgz", + "integrity": "sha1-tBHI18/g3z8v4d+E0b1kpvAEbGg=", + "dev": true, + "requires": { + "gonzales-pe": "3.0.0-28", + "minimatch": "0.2.12", + "vow": "0.4.4", + "vow-fs": "0.3.2" + }, + "dependencies": { + "minimatch": { + "version": "0.2.12", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-0.2.12.tgz", + "integrity": "sha1-6oKgEqxmLH3fqhRPHBR+aUb12vs=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "csv-parse": { + "version": "2.5.0", + "resolved": "http://registry.npm.alibaba-inc.com/csv-parse/download/csv-parse-2.5.0.tgz", + "integrity": "sha1-ZXSJl+zDcZxZRiLbG56g4ut9Vrs=" + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/currently-unhandled/download/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/date-now/download/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "dateformat": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/dateformat/download/dateformat-2.2.0.tgz", + "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.alibaba-inc.com/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/decamelize/download/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/decode-uri-component/download/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/defaults/download/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-2.0.2.tgz", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/is-descriptor/download/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/depd/download/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "deprecated": { + "version": "0.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/deprecated/download/deprecated-0.0.1.tgz", + "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/destroy/download/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/detect-file/download/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/dom-serializer/download/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "http://registry.npm.alibaba-inc.com/domelementtype/download/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/entities/download/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/domelementtype/download/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/domhandler/download/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "http://registry.npm.alibaba-inc.com/domutils/download/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/duplexer/download/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/duplexer2/download/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/ee-first/download/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/encodeurl/download/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "0.1.5", + "resolved": "http://registry.npm.alibaba-inc.com/end-of-stream/download/end-of-stream-0.1.5.tgz", + "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", + "dev": true, + "requires": { + "once": "~1.3.0" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/entities/download/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/error-ex/download/error-ex-1.3.2.tgz", + "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/escape-html/download/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "http://registry.npm.alibaba-inc.com/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/esprima/download/esprima-3.0.0.tgz", + "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=" + }, + "etag": { + "version": "1.7.0", + "resolved": "http://registry.npm.alibaba-inc.com/etag/download/etag-1.7.0.tgz", + "integrity": "sha1-A9MLX2fdbmMtKUXTDWZScxo01dg=", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "http://registry.npm.alibaba-inc.com/event-stream/download/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/exit/download/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/expand-brackets/download/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "http://registry.npm.alibaba-inc.com/expand-range/download/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "http://registry.npm.alibaba-inc.com/fill-range/download/fill-range-2.2.4.tgz", + "integrity": "sha1-6x53OrsFbc2N8r/favWbizqTZWU=", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-number/download/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/isobject/download/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/expand-tilde/download/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/extend/download/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-extendable/download/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/extglob/download/extglob-2.0.4.tgz", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/is-descriptor/download/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fancy-log": { + "version": "1.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/fancy-log/download/fancy-log-1.3.2.tgz", + "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "time-stamp": "^1.0.0" + } + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "http://registry.npm.alibaba-inc.com/faye-websocket/download/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/filename-regex/download/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "filesize": { + "version": "2.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/filesize/download/filesize-2.0.4.tgz", + "integrity": "sha1-eAWUHGD83+Y/RtfqNYxZreEcEyU=", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/fill-range/download/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/finalhandler/download/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/find-index/download/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/find-up/download/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/findup-sync/download/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/fined/download/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/first-chunk-stream/download/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "flagged-respawn": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/flagged-respawn/download/flagged-respawn-1.0.0.tgz", + "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/for-in/download/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/for-own/download/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/fragment-cache/download/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/fresh/download/fresh-0.3.0.tgz", + "integrity": "sha1-ZR+DjiJCTnVm3hYdg1jKoZn4PU8=", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "http://registry.npm.alibaba-inc.com/from/download/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/fs-exists-sync/download/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/fs.realpath/download/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "gaze": { + "version": "0.5.2", + "resolved": "http://registry.npm.alibaba-inc.com/gaze/download/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "dev": true, + "requires": { + "globule": "~0.1.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/get-stdin/download/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "http://registry.npm.alibaba-inc.com/get-value/download/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "4.5.3", + "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^2.0.1", + "once": "^1.3.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/glob-base/download/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/glob-parent/download/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "glob-stream": { + "version": "3.1.18", + "resolved": "http://registry.npm.alibaba-inc.com/glob-stream/download/glob-stream-3.1.18.tgz", + "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", + "dev": true, + "requires": { + "glob": "^4.3.1", + "glob2base": "^0.0.12", + "minimatch": "^2.0.1", + "ordered-read-streams": "^0.1.0", + "through2": "^0.6.1", + "unique-stream": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "http://registry.npm.alibaba-inc.com/through2/download/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "glob-watcher": { + "version": "0.0.6", + "resolved": "http://registry.npm.alibaba-inc.com/glob-watcher/download/glob-watcher-0.0.6.tgz", + "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", + "dev": true, + "requires": { + "gaze": "^0.5.1" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "http://registry.npm.alibaba-inc.com/glob2base/download/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "dev": true, + "requires": { + "find-index": "^0.1.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/global-modules/download/global-modules-1.0.0.tgz", + "integrity": "sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/global-prefix/download/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globule": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/globule/download/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "requires": { + "glob": "~3.1.21", + "lodash": "~1.0.1", + "minimatch": "~0.2.11" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/inherits/download/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/lodash/download/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "glogg": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/glogg/download/glogg-1.0.1.tgz", + "integrity": "sha1-3PdY5EeJzD89MsHzVio2duajSBA=", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "gonzales-pe": { + "version": "3.0.0-28", + "resolved": "http://registry.npm.alibaba-inc.com/gonzales-pe/download/gonzales-pe-3.0.0-28.tgz", + "integrity": "sha1-3VC0HdFbaCooxA5fD/IAeQGsYr0=", + "dev": true + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "dev": true, + "requires": { + "natives": "^1.1.0" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-readlink/download/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "gulp": { + "version": "3.9.1", + "resolved": "http://registry.npm.alibaba-inc.com/gulp/download/gulp-3.9.1.tgz", + "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", + "dev": true, + "requires": { + "archy": "^1.0.0", + "chalk": "^1.0.0", + "deprecated": "^0.0.1", + "gulp-util": "^3.0.0", + "interpret": "^1.0.0", + "liftoff": "^2.1.0", + "minimist": "^1.1.0", + "orchestrator": "^0.3.0", + "pretty-hrtime": "^1.0.0", + "semver": "^4.1.0", + "tildify": "^1.0.0", + "v8flags": "^2.0.2", + "vinyl-fs": "^0.3.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "gulp-clean": { + "version": "0.4.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-clean/download/gulp-clean-0.4.0.tgz", + "integrity": "sha1-O8JecITmQbvXveBXz5DAHFDZWVA=", + "dev": true, + "requires": { + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "rimraf": "^2.6.2", + "through2": "^2.0.3", + "vinyl": "^2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/clone/download/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/clone-stats/download/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/replace-ext/download/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl/download/vinyl-2.2.0.tgz", + "integrity": "sha1-2FsH2pbkWNJbL/4Z/s6fLKoT7YY=", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + } + } + }, + "gulp-concat": { + "version": "2.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-concat/download/gulp-concat-2.6.1.tgz", + "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", + "dev": true, + "requires": { + "concat-with-sourcemaps": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^2.0.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/clone/download/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/clone-stats/download/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/replace-ext/download/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl/download/vinyl-2.2.0.tgz", + "integrity": "sha1-2FsH2pbkWNJbL/4Z/s6fLKoT7YY=", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + } + } + }, + "gulp-connect": { + "version": "5.5.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-connect/download/gulp-connect-5.5.0.tgz", + "integrity": "sha1-5CMU0Hrfmy/7onKXTO/5H3yHYTA=", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "connect": "^3.6.5", + "connect-livereload": "^0.5.4", + "event-stream": "^3.3.2", + "fancy-log": "^1.3.2", + "send": "^0.13.2", + "serve-index": "^1.9.1", + "serve-static": "^1.13.1", + "tiny-lr": "^0.2.1" + } + }, + "gulp-csscomb": { + "version": "3.0.8", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-csscomb/download/gulp-csscomb-3.0.8.tgz", + "integrity": "sha1-3zSCSlgKTH0zUcHo67ateh1aibc=", + "dev": true, + "requires": { + "csscomb": "^3.1.7", + "gulp-util": "^3.0.7", + "through2": "^2.0.1" + } + }, + "gulp-cssmin": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-cssmin/download/gulp-cssmin-0.2.0.tgz", + "integrity": "sha1-h6s8ad05sg1dljVcZQStakR7HnI=", + "dev": true, + "requires": { + "clean-css": "^3.1.9", + "filesize": "~2.0.0", + "graceful-fs": "~4.1.4", + "gulp-rename": "~1.1.0", + "gulp-util": "~2.2.0", + "map-stream": "0.0.4", + "temp-write": "~0.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-regex/download/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/ansi-styles/download/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "http://registry.npm.alibaba-inc.com/chalk/download/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "http://registry.npm.alibaba-inc.com/dateformat/download/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "gulp-util": { + "version": "2.2.20", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-util/download/gulp-util-2.2.20.tgz", + "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "dev": true, + "requires": { + "chalk": "^0.5.0", + "dateformat": "^1.0.7-1.2.3", + "lodash._reinterpolate": "^2.4.1", + "lodash.template": "^2.4.1", + "minimist": "^0.2.0", + "multipipe": "^0.1.0", + "through2": "^0.5.0", + "vinyl": "^0.2.1" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/has-ansi/download/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._reinterpolate/download/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", + "dev": true + }, + "lodash.escape": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.escape/download/lodash.escape-2.4.1.tgz", + "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "dev": true, + "requires": { + "lodash._escapehtmlchar": "~2.4.1", + "lodash._reunescapedhtml": "~2.4.1", + "lodash.keys": "~2.4.1" + } + }, + "lodash.keys": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.keys/download/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + }, + "lodash.template": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.template/download/lodash.template-2.4.1.tgz", + "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", + "dev": true, + "requires": { + "lodash._escapestringchar": "~2.4.1", + "lodash._reinterpolate": "~2.4.1", + "lodash.defaults": "~2.4.1", + "lodash.escape": "~2.4.1", + "lodash.keys": "~2.4.1", + "lodash.templatesettings": "~2.4.1", + "lodash.values": "~2.4.1" + } + }, + "lodash.templatesettings": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.templatesettings/download/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~2.4.1", + "lodash.escape": "~2.4.1" + } + }, + "map-stream": { + "version": "0.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/map-stream/download/map-stream-0.0.4.tgz", + "integrity": "sha1-XsbekCE+9sey65Nn6a3o2k79tos=", + "dev": true + }, + "minimist": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-0.2.0.tgz", + "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/strip-ansi/download/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/supports-color/download/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "through2": { + "version": "0.5.1", + "resolved": "http://registry.npm.alibaba-inc.com/through2/download/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + } + }, + "vinyl": { + "version": "0.2.3", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl/download/vinyl-0.2.3.tgz", + "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "dev": true, + "requires": { + "clone-stats": "~0.0.1" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/xtend/download/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "gulp-jshint": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-jshint/download/gulp-jshint-2.1.0.tgz", + "integrity": "sha1-v6+Sf3ju4mPFu6xfY+MU1Ep71B4=", + "dev": true, + "requires": { + "lodash": "^4.12.0", + "minimatch": "^3.0.3", + "plugin-error": "^0.1.2", + "rcloader": "^0.2.2", + "through2": "^2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "gulp-load-plugins": { + "version": "1.5.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-load-plugins/download/gulp-load-plugins-1.5.0.tgz", + "integrity": "sha1-TEGffldk2aDjMGG6uWGPgbc9QXE=", + "dev": true, + "requires": { + "array-unique": "^0.2.1", + "fancy-log": "^1.2.0", + "findup-sync": "^0.4.0", + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "micromatch": "^2.3.8", + "resolve": "^1.1.7" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/arr-diff/download/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/array-unique/download/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "http://registry.npm.alibaba-inc.com/braces/download/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "detect-file": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/detect-file/download/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "dev": true, + "requires": { + "fs-exists-sync": "^0.1.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "http://registry.npm.alibaba-inc.com/expand-brackets/download/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-tilde": { + "version": "1.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/expand-tilde/download/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "dev": true, + "requires": { + "os-homedir": "^1.0.1" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/extglob/download/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "findup-sync": { + "version": "0.4.3", + "resolved": "http://registry.npm.alibaba-inc.com/findup-sync/download/findup-sync-0.4.3.tgz", + "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", + "dev": true, + "requires": { + "detect-file": "^0.1.0", + "is-glob": "^2.0.1", + "micromatch": "^2.3.7", + "resolve-dir": "^0.1.0" + } + }, + "global-modules": { + "version": "0.2.3", + "resolved": "http://registry.npm.alibaba-inc.com/global-modules/download/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "dev": true, + "requires": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + } + }, + "global-prefix": { + "version": "0.1.5", + "resolved": "http://registry.npm.alibaba-inc.com/global-prefix/download/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-windows": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-windows/download/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "http://registry.npm.alibaba-inc.com/micromatch/download/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "resolve-dir": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/resolve-dir/download/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "dev": true, + "requires": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + } + } + } + }, + "gulp-rename": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-rename/download/gulp-rename-1.1.0.tgz", + "integrity": "sha1-kwkKqvTThsB/IFOKaIjxXvunJ6E=", + "dev": true, + "requires": { + "map-stream": ">=0.0.4" + } + }, + "gulp-serv": { + "version": "0.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-serv/download/gulp-serv-0.0.1.tgz", + "integrity": "sha1-8bNhdWfusKUkW2Vw+dSjqBmcZ04=", + "dev": true, + "requires": { + "connect": "^3.3.3", + "serve-static": "^1.7.1" + } + }, + "gulp-uglify": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-uglify/download/gulp-uglify-3.0.1.tgz", + "integrity": "sha1-jT7uRmUhvqaxD9dd/3Kt+LfqLZc=", + "dev": true, + "requires": { + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "lodash": "^4.13.1", + "make-error-cause": "^1.1.1", + "safe-buffer": "^5.1.2", + "through2": "^2.0.0", + "uglify-js": "^3.0.5", + "vinyl-sourcemaps-apply": "^0.2.0" + } + }, + "gulp-util": { + "version": "3.0.8", + "resolved": "http://registry.npm.alibaba-inc.com/gulp-util/download/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "dev": true, + "requires": { + "array-differ": "^1.0.0", + "array-uniq": "^1.0.2", + "beeper": "^1.0.0", + "chalk": "^1.0.0", + "dateformat": "^2.0.0", + "fancy-log": "^1.1.0", + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "lodash._reescape": "^3.0.0", + "lodash._reevaluate": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.template": "^3.0.0", + "minimist": "^1.1.0", + "multipipe": "^0.1.2", + "object-assign": "^3.0.0", + "replace-ext": "0.0.1", + "through2": "^2.0.0", + "vinyl": "^0.5.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/gulplog/download/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "^1.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/has-ansi/download/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/has-gulplog/download/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/has-value/download/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/has-values/download/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/homedir-polyfill/download/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "http://registry.npm.alibaba-inc.com/hosted-git-info/download/hosted-git-info-2.7.1.tgz", + "integrity": "sha1-l/I2l3vW4SVAiTD/bePuxigewEc=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "http://registry.npm.alibaba-inc.com/htmlparser2/download/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "http-errors": { + "version": "1.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/http-errors/download/http-errors-1.3.1.tgz", + "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "statuses": "1" + } + }, + "http-parser-js": { + "version": "0.4.13", + "resolved": "http://registry.npm.alibaba-inc.com/http-parser-js/download/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", + "dev": true + }, + "humanize": { + "version": "0.0.9", + "resolved": "http://registry.npm.alibaba-inc.com/humanize/download/humanize-0.0.9.tgz", + "integrity": "sha1-GZT/rs3+nEQe0r2sdFK3u0yeQaQ=" + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "http://registry.npm.alibaba-inc.com/iconv-lite/download/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/indent-string/download/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "http://registry.npm.alibaba-inc.com/inflight/download/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/inherits/download/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "http://registry.npm.alibaba-inc.com/ini/download/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=", + "dev": true + }, + "interpret": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/interpret/download/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-absolute/download/is-absolute-1.0.0.tgz", + "integrity": "sha1-OV4a6EsR8mrReV5zwXN45IowFXY=", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "http://registry.npm.alibaba-inc.com/is-accessor-descriptor/download/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-arrayish/download/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "http://registry.npm.alibaba-inc.com/is-buffer/download/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-builtin-module/download/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/is-data-descriptor/download/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "http://registry.npm.alibaba-inc.com/is-descriptor/download/is-descriptor-0.1.6.tgz", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-5.1.0.tgz", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/is-dotfile/download/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "http://registry.npm.alibaba-inc.com/is-equal-shallow/download/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-extendable/download/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/is-finite/download/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-number/download/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/is-plain-object/download/is-plain-object-2.0.4.tgz", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-posix-bracket/download/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-primitive/download/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-relative": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-relative/download/is-relative-1.0.0.tgz", + "integrity": "sha1-obtpNc6MXboei5dUubLcwCDiJg0=", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-unc-path/download/is-unc-path-1.0.0.tgz", + "integrity": "sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-utf8/download/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/is-windows/download/is-windows-1.0.2.tgz", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/isexe/download/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/isobject/download/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "jquery": { + "version": "3.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/jquery/download/jquery-3.3.1.tgz", + "integrity": "sha1-lYzinoHJeQ8xvneS311NlfxX+8o=" + }, + "jshint": { + "version": "2.9.5", + "resolved": "http://registry.npm.alibaba-inc.com/jshint/download/jshint-2.9.5.tgz", + "integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=", + "dev": true, + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "3.7.x", + "minimatch": "~3.0.2", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x" + }, + "dependencies": { + "lodash": { + "version": "3.7.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash/download/lodash-3.7.0.tgz", + "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + }, + "liftoff": { + "version": "2.5.0", + "resolved": "http://registry.npm.alibaba-inc.com/liftoff/download/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^2.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "livereload-js": { + "version": "2.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/livereload-js/download/livereload-js-2.3.0.tgz", + "integrity": "sha1-w6si6Kr1vzUF2A0JjLrWdyZUjJo=", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/load-json-file/download/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/strip-bom/download/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "http://registry.npm.alibaba-inc.com/lodash/download/lodash-4.17.10.tgz", + "integrity": "sha1-G3eTz3JZ6jj7NmHU04syYK+K5Oc=" + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._basecopy/download/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._basetostring/download/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._basevalues/download/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", + "dev": true + }, + "lodash._escapehtmlchar": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._escapehtmlchar/download/lodash._escapehtmlchar-2.4.1.tgz", + "integrity": "sha1-32fDu2t+jh6DGrSL+geVuSr+iZ0=", + "dev": true, + "requires": { + "lodash._htmlescapes": "~2.4.1" + } + }, + "lodash._escapestringchar": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._escapestringchar/download/lodash._escapestringchar-2.4.1.tgz", + "integrity": "sha1-7P4iYYoq3lC/7qQ5N+Ud9m8O23I=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._getnative/download/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._htmlescapes": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._htmlescapes/download/lodash._htmlescapes-2.4.1.tgz", + "integrity": "sha1-MtFL8IRLbeb4tioFG09nwii2JMs=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._isiterateecall/download/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash._isnative": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._isnative/download/lodash._isnative-2.4.1.tgz", + "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=", + "dev": true + }, + "lodash._objecttypes": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._objecttypes/download/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._reescape/download/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._reevaluate/download/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._reinterpolate/download/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash._reunescapedhtml": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._reunescapedhtml/download/lodash._reunescapedhtml-2.4.1.tgz", + "integrity": "sha1-dHxPxAED6zu4oJduVx96JlnpO6c=", + "dev": true, + "requires": { + "lodash._htmlescapes": "~2.4.1", + "lodash.keys": "~2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.keys/download/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + } + } + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._root/download/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "dev": true + }, + "lodash._shimkeys": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash._shimkeys/download/lodash._shimkeys-2.4.1.tgz", + "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", + "dev": true, + "requires": { + "lodash._objecttypes": "~2.4.1" + } + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.assign/download/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.clonedeep/download/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.defaults": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.defaults/download/lodash.defaults-2.4.1.tgz", + "integrity": "sha1-p+iIXwXmiFEUS24SqPNngCa8TFQ=", + "dev": true, + "requires": { + "lodash._objecttypes": "~2.4.1", + "lodash.keys": "~2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.keys/download/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + } + } + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.escape/download/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "dev": true, + "requires": { + "lodash._root": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.isarguments/download/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.isarray/download/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isobject": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.isobject/download/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "dev": true, + "requires": { + "lodash._objecttypes": "~2.4.1" + } + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.keys/download/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.merge/download/lodash.merge-4.6.1.tgz", + "integrity": "sha1-rcJdnLmbk5HFliTzefu6YNcRHVQ=", + "dev": true + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.restparam/download/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.template/download/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash._basetostring": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0", + "lodash.keys": "^3.0.0", + "lodash.restparam": "^3.0.0", + "lodash.templatesettings": "^3.0.0" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.templatesettings/download/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0" + } + }, + "lodash.values": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.values/download/lodash.values-2.4.1.tgz", + "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", + "dev": true, + "requires": { + "lodash.keys": "~2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.keys/download/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + } + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "http://registry.npm.alibaba-inc.com/loud-rejection/download/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "http://registry.npm.alibaba-inc.com/lru-cache/download/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "make-error": { + "version": "1.3.4", + "resolved": "http://registry.npm.alibaba-inc.com/make-error/download/make-error-1.3.4.tgz", + "integrity": "sha1-GZeO1XX56VRdL/jBPjO10Ypn1TU=", + "dev": true + }, + "make-error-cause": { + "version": "1.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/make-error-cause/download/make-error-cause-1.2.2.tgz", + "integrity": "sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0=", + "dev": true, + "requires": { + "make-error": "^1.2.0" + } + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/make-iterator/download/make-iterator-1.0.1.tgz", + "integrity": "sha1-KbM/MSqo9UfEpeSQ9Wr87JkTOtY=", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/map-cache/download/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/map-obj/download/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/map-stream/download/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/map-visit/download/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/math-random/download/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/media-typer/download/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "http://registry.npm.alibaba-inc.com/meow/download/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/object-assign/download/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "http://registry.npm.alibaba-inc.com/micromatch/download/micromatch-3.1.10.tgz", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "microplugin": { + "version": "0.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/microplugin/download/microplugin-0.0.3.tgz", + "integrity": "sha1-H8Lhu3yeGegr2Eu6kTe75xJQ2M0=" + }, + "mime": { + "version": "1.3.4", + "resolved": "http://registry.npm.alibaba-inc.com/mime/download/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", + "dev": true + }, + "mime-db": { + "version": "1.35.0", + "resolved": "http://registry.npm.alibaba-inc.com/mime-db/download/mime-db-1.35.0.tgz", + "integrity": "sha1-BWnWV0ZkkSg3CWY603mpm5DZq0c=", + "dev": true + }, + "mime-types": { + "version": "2.1.19", + "resolved": "http://registry.npm.alibaba-inc.com/mime-types/download/mime-types-2.1.19.tgz", + "integrity": "sha1-ceRkU3p++BwV8tudl+kT/A/2BvA=", + "dev": true, + "requires": { + "mime-db": "~1.35.0" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/mixin-deep/download/mixin-deep-1.3.1.tgz", + "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-extendable/download/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npm.alibaba-inc.com/mkdirp/download/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npm.alibaba-inc.com/minimist/download/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "moment": { + "version": "2.22.2", + "resolved": "http://registry.npm.alibaba-inc.com/moment/download/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "ms": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/ms/download/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multipipe": { + "version": "0.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/multipipe/download/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "dev": true, + "requires": { + "duplexer2": "0.0.2" + } + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "http://registry.npm.alibaba-inc.com/nanomatch/download/nanomatch-1.2.13.tgz", + "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natives": { + "version": "1.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/natives/download/natives-1.1.4.tgz", + "integrity": "sha1-Lw8iT8mn3VNAfHZnyEz42+dz3lg=", + "dev": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/negotiator/download/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "ng-dialog": { + "version": "0.6.6", + "resolved": "http://registry.npm.alibaba-inc.com/ng-dialog/download/ng-dialog-0.6.6.tgz", + "integrity": "sha1-qtekfI5ByAf3AxPbEpbb6d4x2n0=" + }, + "ng-tags-input": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/ng-tags-input/download/ng-tags-input-3.0.0.tgz", + "integrity": "sha1-X7BgMuAtOKrQLUQ+P1r8UeEn+ks=" + }, + "node-uuid": { + "version": "1.4.0", + "resolved": "http://registry.npm.alibaba-inc.com/node-uuid/download/node-uuid-1.4.0.tgz", + "integrity": "sha1-B/myM3Vy/2J1x3Xh1IUT86RdemU=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "http://registry.npm.alibaba-inc.com/normalize-package-data/download/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/normalize-path/download/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/number-is-nan/download/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/object-assign/download/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/object-copy/download/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/object-visit/download/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/object.defaults/download/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/object.map/download/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/object.omit/download/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "http://registry.npm.alibaba-inc.com/for-own/download/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/object.pick/download/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "oclazyload": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/oclazyload/download/oclazyload-1.1.0.tgz", + "integrity": "sha1-qYBzIvGQggqBwCLy7xcB0DbYPoc=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/on-finished/download/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.3.3", + "resolved": "http://registry.npm.alibaba-inc.com/once/download/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "open": { + "version": "0.0.5", + "resolved": "http://registry.npm.alibaba-inc.com/open/download/open-0.0.5.tgz", + "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/optimist/download/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "orchestrator": { + "version": "0.3.8", + "resolved": "http://registry.npm.alibaba-inc.com/orchestrator/download/orchestrator-0.3.8.tgz", + "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", + "dev": true, + "requires": { + "end-of-stream": "~0.1.5", + "sequencify": "~0.0.7", + "stream-consume": "~0.1.0" + } + }, + "ordered-read-streams": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/ordered-read-streams/download/ordered-read-streams-0.1.0.tgz", + "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/os-homedir/download/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/parse-filepath/download/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/parse-glob/download/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-extglob/download/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/is-glob/download/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/parse-json/download/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/parse-passwd/download/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/parseurl/download/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/pascalcase/download/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/path-exists/download/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/path-is-absolute/download/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "http://registry.npm.alibaba-inc.com/path-parse/download/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-root": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/path-root/download/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/path-root-regex/download/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/path-type/download/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + } + } + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "http://registry.npm.alibaba-inc.com/pause-stream/download/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/pify/download/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/pinkie/download/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/pinkie-promise/download/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/plugin-error/download/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/arr-diff/download/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/arr-union/download/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "http://registry.npm.alibaba-inc.com/array-slice/download/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "^1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/posix-character-classes/download/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/preserve/download/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/pretty-hrtime/download/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/process-nextick-args/download/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", + "dev": true + }, + "qs": { + "version": "5.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/qs/download/qs-5.1.0.tgz", + "integrity": "sha1-TZMuXH6kEcynajEtOaYGIA/VDNk=", + "dev": true + }, + "randomatic": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/randomatic/download/randomatic-3.0.0.tgz", + "integrity": "sha1-01SQAw6091eN4pLObfsEqRoSiSM=", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-number/download/is-number-4.0.0.tgz", + "integrity": "sha1-ACbjf1RU1z41bf5lZGmYZ8an8P8=", + "dev": true + } + } + }, + "range-parser": { + "version": "1.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/range-parser/download/range-parser-1.0.3.tgz", + "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=", + "dev": true + }, + "raw-body": { + "version": "2.1.7", + "resolved": "http://registry.npm.alibaba-inc.com/raw-body/download/raw-body-2.1.7.tgz", + "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", + "dev": true, + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.13", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "2.4.0", + "resolved": "http://registry.npm.alibaba-inc.com/bytes/download/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", + "dev": true + } + } + }, + "rcfinder": { + "version": "0.1.9", + "resolved": "http://registry.npm.alibaba-inc.com/rcfinder/download/rcfinder-0.1.9.tgz", + "integrity": "sha1-8+gPOH3fmugK4wpBADKWQuroERU=", + "dev": true, + "requires": { + "lodash.clonedeep": "^4.3.2" + } + }, + "rcloader": { + "version": "0.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/rcloader/download/rcloader-0.2.2.tgz", + "integrity": "sha1-WNIpi0YtC5v9ITPSoex0+9cFxxc=", + "dev": true, + "requires": { + "lodash.assign": "^4.2.0", + "lodash.isobject": "^3.0.2", + "lodash.merge": "^4.6.0", + "rcfinder": "^0.1.6" + }, + "dependencies": { + "lodash.isobject": { + "version": "3.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/lodash.isobject/download/lodash.isobject-3.0.2.tgz", + "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/read-pkg/download/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/read-pkg-up/download/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "http://registry.npm.alibaba-inc.com/rechoir/download/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/redent/download/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "redeyed": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/redeyed/download/redeyed-1.0.1.tgz", + "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "requires": { + "esprima": "~3.0.0" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "http://registry.npm.alibaba-inc.com/regex-cache/download/regex-cache-0.4.4.tgz", + "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/regex-not/download/regex-not-1.0.2.tgz", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/remove-trailing-separator/download/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/repeat-element/download/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/repeat-string/download/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/repeating/download/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/replace-ext/download/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "http://registry.npm.alibaba-inc.com/resolve/download/resolve-1.8.1.tgz", + "integrity": "sha1-gvHsGaQjrB+9CAsLqwa6NuhKeiY=", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/resolve-dir/download/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/resolve-url/download/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "http://registry.npm.alibaba-inc.com/ret/download/ret-0.1.15.tgz", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "http://registry.npm.alibaba-inc.com/rimraf/download/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, + "requires": { + "glob": "^7.0.5" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/safe-buffer/download/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/safe-regex/download/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "selectize": { + "version": "0.12.6", + "resolved": "http://registry.npm.alibaba-inc.com/selectize/download/selectize-0.12.6.tgz", + "integrity": "sha1-ws8Iy6pMsGxembtFKRnXGwgGkNY=", + "requires": { + "microplugin": "0.0.3", + "sifter": "^0.5.1" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "http://registry.npm.alibaba-inc.com/semver/download/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "send": { + "version": "0.13.2", + "resolved": "http://registry.npm.alibaba-inc.com/send/download/send-0.13.2.tgz", + "integrity": "sha1-dl52B8gFVFK7pvCwUllTUJhgNt4=", + "dev": true, + "requires": { + "debug": "~2.2.0", + "depd": "~1.1.0", + "destroy": "~1.0.4", + "escape-html": "~1.0.3", + "etag": "~1.7.0", + "fresh": "0.3.0", + "http-errors": "~1.3.1", + "mime": "1.3.4", + "ms": "0.7.1", + "on-finished": "~2.3.0", + "range-parser": "~1.0.3", + "statuses": "~1.2.1" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/debug/download/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "http://registry.npm.alibaba-inc.com/ms/download/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "statuses": { + "version": "1.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/statuses/download/statuses-1.2.1.tgz", + "integrity": "sha1-3e1FzBglbVHtQK7BQkidXGECbSg=", + "dev": true + } + } + }, + "sequencify": { + "version": "0.0.7", + "resolved": "http://registry.npm.alibaba-inc.com/sequencify/download/sequencify-0.0.7.tgz", + "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "http://registry.npm.alibaba-inc.com/serve-index/download/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "http://registry.npm.alibaba-inc.com/http-errors/download/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "http://registry.npm.alibaba-inc.com/statuses/download/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "http://registry.npm.alibaba-inc.com/serve-static/download/serve-static-1.13.2.tgz", + "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "dependencies": { + "etag": { + "version": "1.8.1", + "resolved": "http://registry.npm.alibaba-inc.com/etag/download/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "http://registry.npm.alibaba-inc.com/fresh/download/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "http://registry.npm.alibaba-inc.com/http-errors/download/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "http://registry.npm.alibaba-inc.com/mime/download/mime-1.4.1.tgz", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=", + "dev": true + }, + "range-parser": { + "version": "1.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/range-parser/download/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "send": { + "version": "0.16.2", + "resolved": "http://registry.npm.alibaba-inc.com/send/download/send-0.16.2.tgz", + "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "http://registry.npm.alibaba-inc.com/statuses/download/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=", + "dev": true + } + } + }, + "set-value": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/set-value/download/set-value-2.0.0.tgz", + "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/setprototypeof/download/setprototypeof-1.1.0.tgz", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=", + "dev": true + }, + "shelljs": { + "version": "0.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/shelljs/download/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true + }, + "sifter": { + "version": "0.5.3", + "resolved": "http://registry.npm.alibaba-inc.com/sifter/download/sifter-0.5.3.tgz", + "integrity": "sha1-XmUH/owRSyso2Qtr9OW2NuYR5Is=", + "requires": { + "async": "^2.6.0", + "cardinal": "^1.0.0", + "csv-parse": "^2.0.0", + "humanize": "^0.0.9", + "optimist": "^0.6.1" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/sigmund/download/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/signal-exit/download/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "http://registry.npm.alibaba-inc.com/snapdragon/download/snapdragon-0.8.2.tgz", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/snapdragon-node/download/snapdragon-node-2.1.1.tgz", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/is-descriptor/download/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/snapdragon-util/download/snapdragon-util-3.0.1.tgz", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.7.3.tgz", + "integrity": "sha1-UwL4FpAxc1ImVECS5kmB91F1A4M=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "http://registry.npm.alibaba-inc.com/source-map-resolve/download/source-map-resolve-0.5.2.tgz", + "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "http://registry.npm.alibaba-inc.com/source-map-url/download/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sparkles": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/sparkles/download/sparkles-1.0.1.tgz", + "integrity": "sha1-AI22XtzmxQ7sDF4ijhlFBh3QQ3w=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/spdx-correct/download/spdx-correct-3.0.0.tgz", + "integrity": "sha1-BaW01xU6GVvJLDxCW2nzsqlSTII=", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/spdx-exceptions/download/spdx-exceptions-2.1.0.tgz", + "integrity": "sha1-LHrmEFbHFKW5ubKyr30xHvXHj+k=", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/spdx-expression-parse/download/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/spdx-license-ids/download/spdx-license-ids-3.0.0.tgz", + "integrity": "sha1-enzShHDMbToc/m1miG9rxDDTrIc=", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "http://registry.npm.alibaba-inc.com/split/download/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/split-string/download/split-string-3.1.0.tgz", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/static-extend/download/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "http://registry.npm.alibaba-inc.com/define-property/download/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/statuses/download/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/stream-combiner/download/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + }, + "stream-consume": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/stream-consume/download/stream-consume-0.1.1.tgz", + "integrity": "sha1-0721mMK9CugrjKx6xQsRB6eZbEg=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "http://registry.npm.alibaba-inc.com/string_decoder/download/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/strip-ansi/download/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/strip-bom/download/strip-bom-1.0.0.tgz", + "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", + "dev": true, + "requires": { + "first-chunk-stream": "^1.0.0", + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/strip-indent/download/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "http://registry.npm.alibaba-inc.com/strip-json-comments/download/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/supports-color/download/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "temp-write": { + "version": "0.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/temp-write/download/temp-write-0.1.1.tgz", + "integrity": "sha1-C2Rng43Xf79/YqDJPah5cy/9qTI=", + "dev": true, + "requires": { + "graceful-fs": "~2.0.0", + "tempfile": "~0.1.2" + }, + "dependencies": { + "graceful-fs": { + "version": "2.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/graceful-fs/download/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + } + } + }, + "tempfile": { + "version": "0.1.3", + "resolved": "http://registry.npm.alibaba-inc.com/tempfile/download/tempfile-0.1.3.tgz", + "integrity": "sha1-fWtxAEcznTn4RzJ6BW2t8YMQMBA=", + "dev": true, + "requires": { + "uuid": "~1.4.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npm.alibaba-inc.com/through/download/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/through2/download/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/string_decoder/download/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "tildify": { + "version": "1.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/tildify/download/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/time-stamp/download/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "tiny-lr": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/tiny-lr/download/tiny-lr-0.2.1.tgz", + "integrity": "sha1-s/26gC5dVqM8L28QeUsy5Hescp0=", + "dev": true, + "requires": { + "body-parser": "~1.14.0", + "debug": "~2.2.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.2.0", + "parseurl": "~1.3.0", + "qs": "~5.1.0" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/debug/download/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "http://registry.npm.alibaba-inc.com/ms/download/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "http://registry.npm.alibaba-inc.com/to-object-path/download/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.alibaba-inc.com/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/to-regex/download/to-regex-3.0.2.tgz", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/to-regex-range/download/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/trim-newlines/download/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "http://registry.npm.alibaba-inc.com/type-is/download/type-is-1.6.16.tgz", + "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "uglify-js": { + "version": "3.4.5", + "resolved": "http://registry.npm.alibaba-inc.com/uglify-js/download/uglify-js-3.4.5.tgz", + "integrity": "sha1-ZQiJwHZs8Pb9U0bOoJzSEvVEvmk=", + "dev": true, + "requires": { + "commander": "~2.16.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.16.0", + "resolved": "http://registry.npm.alibaba-inc.com/commander/download/commander-2.16.0.tgz", + "integrity": "sha1-8WOQWTmWzrTz7rAgsx14Uo9/ilA=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "dev": true + } + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "http://registry.npm.alibaba-inc.com/unc-path-regex/download/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/union-value/download/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/extend-shallow/download/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "http://registry.npm.alibaba-inc.com/set-value/download/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unique-stream": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/unique-stream/download/unique-stream-1.0.0.tgz", + "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/unpipe/download/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/unset-value/download/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/has-value/download/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/isobject/download/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "http://registry.npm.alibaba-inc.com/has-values/download/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "http://registry.npm.alibaba-inc.com/isarray/download/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "http://registry.npm.alibaba-inc.com/urix/download/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/use/download/use-3.1.1.tgz", + "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", + "dev": true + }, + "user-home": { + "version": "1.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/user-home/download/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/util-deprecate/download/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/utils-merge/download/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "1.4.2", + "resolved": "http://registry.npm.alibaba-inc.com/uuid/download/uuid-1.4.2.tgz", + "integrity": "sha1-RTAZ9oaWam34PNxSROfJkOzDMvw=", + "dev": true + }, + "v8flags": { + "version": "2.1.1", + "resolved": "http://registry.npm.alibaba-inc.com/v8flags/download/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "^1.1.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/validate-npm-package-license/download/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha1-gWQ7y+8b3+zUYjeT3EZIlIupgzg=", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vinyl": { + "version": "0.5.3", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl/download/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-fs": { + "version": "0.3.14", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl-fs/download/vinyl-fs-0.3.14.tgz", + "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", + "dev": true, + "requires": { + "defaults": "^1.0.0", + "glob-stream": "^3.1.5", + "glob-watcher": "^0.0.6", + "graceful-fs": "^3.0.0", + "mkdirp": "^0.5.0", + "strip-bom": "^1.0.0", + "through2": "^0.6.1", + "vinyl": "^0.4.0" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "http://registry.npm.alibaba-inc.com/clone/download/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npm.alibaba-inc.com/readable-stream/download/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "http://registry.npm.alibaba-inc.com/through2/download/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl/download/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "http://registry.npm.alibaba-inc.com/vinyl-sourcemaps-apply/download/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "dev": true, + "requires": { + "source-map": "^0.5.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "http://registry.npm.alibaba-inc.com/source-map/download/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "vow": { + "version": "0.4.4", + "resolved": "http://registry.npm.alibaba-inc.com/vow/download/vow-0.4.4.tgz", + "integrity": "sha1-yf5GCRKdf1qmIVCOvmS1HJW8e5g=", + "dev": true + }, + "vow-fs": { + "version": "0.3.2", + "resolved": "http://registry.npm.alibaba-inc.com/vow-fs/download/vow-fs-0.3.2.tgz", + "integrity": "sha1-6isDTYXh24wnfrLpqG0cFfXTjno=", + "dev": true, + "requires": { + "glob": "3.2.8", + "node-uuid": "1.4.0", + "vow": "0.4.4", + "vow-queue": "0.3.1" + }, + "dependencies": { + "glob": { + "version": "3.2.8", + "resolved": "http://registry.npm.alibaba-inc.com/glob/download/glob-3.2.8.tgz", + "integrity": "sha1-VQb0MRchvMYYx9jboUQYh1AwcHM=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "~0.2.11" + } + }, + "minimatch": { + "version": "0.2.14", + "resolved": "http://registry.npm.alibaba-inc.com/minimatch/download/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "vow-queue": { + "version": "0.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/vow-queue/download/vow-queue-0.3.1.tgz", + "integrity": "sha1-WYxRoVsKgabV/AX0dhzrRi3h6Gg=", + "dev": true, + "requires": { + "vow": "~0.4.0" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "http://registry.npm.alibaba-inc.com/websocket-driver/download/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "http://registry.npm.alibaba-inc.com/websocket-extensions/download/websocket-extensions-0.1.3.tgz", + "integrity": "sha1-XS/yKXcAPsaHpLhwc9+7rBRszyk=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "http://registry.npm.alibaba-inc.com/which/download/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "http://registry.npm.alibaba-inc.com/wordwrap/download/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "http://registry.npm.alibaba-inc.com/wrappy/download/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "http://registry.npm.alibaba-inc.com/xtend/download/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + } + } +} diff --git a/sentinel-dashboard/src/main/webapp/resources/package.json b/sentinel-dashboard/src/main/webapp/resources/package.json new file mode 100755 index 00000000..952ed5ca --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/package.json @@ -0,0 +1,54 @@ +{ + "name": "sentinel-dashboard", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo no test case", + "build": "gulp build", + "start": "gulp" + }, + "author": "x-cold ", + "license": "MIT", + "dependencies": { + "angular": "^1.4.8", + "angular-animate": "^1.4.0", + "angular-bootstrap": "^0.12.2", + "angular-clipboard": "^1.6.2", + "angular-cookies": "^1.4.0", + "angular-date-time-input": "^1.2.1", + "angular-loading-bar": "^0.9.0", + "angular-mocks": "^1.4.0", + "angular-resource": "^1.4.0", + "angular-route": "^1.4.0", + "angular-selectize2": "^v1.2.3", + "angular-table-resize": "^2.0.1", + "angular-touch": "^1.4.0", + "angular-ui-notification": "^0.3.6", + "angular-ui-router": "^1.0.18", + "angular-utils-pagination": "^0.11.1", + "angularjs-bootstrap-datetimepicker": "^1.1.4", + "bootstrap-switch": "^3.3.4", + "bootstrap-tagsinput": "~0.7.1", + "moment": "^2.12.0", + "ng-dialog": "^0.6.6", + "ng-tags-input": "~3.0.0", + "oclazyload": "^1.1.0", + "selectize": "^0.12.1" + }, + "devDependencies": { + "gulp": "^3.9.1", + "gulp-clean": "^0.4.0", + "gulp-concat": "^2.6.1", + "gulp-connect": "^5.5.0", + "gulp-csscomb": "^3.0.8", + "gulp-cssmin": "^0.2.0", + "gulp-jshint": "^2.1.0", + "gulp-load-plugins": "^1.5.0", + "gulp-serv": "0.0.1", + "gulp-uglify": "^3.0.0", + "jshint": "^2.9.5", + "open": "0.0.5", + "source-map": "^0.7.3" + } +} diff --git a/sentinel-demo/README.md b/sentinel-demo/README.md new file mode 100755 index 00000000..1849b835 --- /dev/null +++ b/sentinel-demo/README.md @@ -0,0 +1,9 @@ +# Sentinel Examples + +The examples demonstrate: + +- How to use basic functions (e.g. flow control, circuit breaking, load protection) of Sentinel +- How to use file as the dynamic datasource of the rules in Sentinel +- How to use Dubbo with Sentinel +- How to use Apache RocketMQ client with Sentinel + diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml new file mode 100755 index 00000000..2319fabd --- /dev/null +++ b/sentinel-demo/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-parent + 0.1.0 + + sentinel-demo + pom + sentinel-demo + + + sentinel-demo-basic + sentinel-demo-dynamic-file-rule + sentinel-demo-rocketmq + sentinel-demo-dubbo + + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-datasource-extension + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-basic/pom.xml b/sentinel-demo/sentinel-demo-basic/pom.xml new file mode 100755 index 00000000..a7ec4ce8 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-demo + 0.1.0 + + sentinel-demo-basic + + + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java new file mode 100755 index 00000000..6139ba76 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/ExceptionRatioDegradeDemo.java @@ -0,0 +1,170 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.degrade; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.util.TimeUtil; + +/** + *

    + * Degrade is used when the resources are in an unstable state, these resources + * will be degraded within the next defined time window. There are two ways to + * measure whether a resource is stable or not: + *

      + *
    • + * Exception ratio: When the ratio of exception count per second and the success + * qps exceeds the threshold , access to the resource will be blocked in the + * coming window. + *
    • + *
    • + * For average response time, see {@link RtDegradeDemo}. + *
    • + *
    + *

    + * + * @author jialiang.linjl + */ +public class ExceptionRatioDegradeDemo { + + private static final String KEY = "abc"; + + private static AtomicInteger total = new AtomicInteger(); + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger bizException = new AtomicInteger(); + + private static volatile boolean stop = false; + private static final int threadCount = 1; + private static int seconds = 60 + 40; + + public static void main(String[] args) throws Exception { + tick(); + initDegradeRule(); + + for (int i = 0; i < threadCount; i++) { + Thread entryThread = new Thread(new Runnable() { + + @Override + public void run() { + int count = 0; + while (true) { + count++; + Entry entry = null; + try { + Thread.sleep(20); + entry = SphU.entry(KEY); + // token acquired, means pass + pass.addAndGet(1); + if (count % 2 == 0) { + // biz code raise an exception. + throw new RuntimeException("throw runtime "); + } + } catch (BlockException e) { + block.addAndGet(1); + } catch (Throwable t) { + bizException.incrementAndGet(); + Tracer.trace(t); + } finally { + total.addAndGet(1); + if (entry != null) { + entry.exit(); + } + } + } + } + + }); + entryThread.setName("working-thread"); + entryThread.start(); + } + + } + + private static void initDegradeRule() { + List rules = new ArrayList(); + DegradeRule rule = new DegradeRule(); + rule.setResource(KEY); + // set limit exception ratio to 0.1 + rule.setCount(0.1); + rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION); + rule.setTimeWindow(10); + rules.add(rule); + DegradeRuleManager.loadRules(rules); + } + + private static void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + static class TimerTask implements Runnable { + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("begin to statistic!!!"); + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + long oldBizException = 0; + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + long globalBizException = bizException.get(); + long oneSecondBizException = globalBizException - oldBizException; + oldBizException = globalBizException; + + System.out.println(TimeUtil.currentTimeMillis() + ", oneSecondTotal:" + oneSecondTotal + + ", oneSecondPass:" + oneSecondPass + + ", oneSecondBlock:" + oneSecondBlock + + ", oneSecondBizException:" + oneSecondBizException); + if (seconds-- <= 0) { + stop = true; + } + } + long cost = System.currentTimeMillis() - start; + System.out.println("time cost: " + cost + " ms"); + System.out.println("total:" + total.get() + ", pass:" + pass.get() + + ", block:" + block.get() + ", bizException:" + bizException.get()); + System.exit(0); + } + } +} diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/RtDegradeDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/RtDegradeDemo.java new file mode 100755 index 00000000..3236a7c6 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/degrade/RtDegradeDemo.java @@ -0,0 +1,184 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.degrade; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; + +/** + *

    + * Degrade is used when the resources are in an unstable state, these resources + * will be degraded within the next defined time window. There are two ways to + * measure whether a resource is stable or not: + *

      + *
    • + * Average Response Time ('DegradeRule.Grade=RuleContants.DEGRADE_GRADE_RT'): When the + * average RT exceeds the threshold ('count' in 'DegradeRule', ms), the resource + * enters a quasi-degraded state. If the RT of next coming five requests still + * exceed this threshold, this resource will be downgraded, which means that in + * the next time window(Defined in 'timeWindow', s units) all the access to this + * resource will be blocked. + *
    • + *
    • + * Exception ratio, see {@link ExceptionRatioDegradeDemo}. + *
    • + *
    + * + *

    + * + * Run this demo, and the out put will be like: + * + *
    + * 1529399827825,total:0, pass:0, block:0
    + * 1529399828825,total:4263, pass:100, block:4164
    + * 1529399829825,total:19179, pass:4, block:19176
    + * 1529399830824,total:19806, pass:0, block:19806  //begin degrade
    + * 1529399831825,total:19198, pass:0, block:19198
    + * 1529399832824,total:19481, pass:0, block:19481
    + * 1529399833826,total:19241, pass:0, block:19241
    + * 1529399834826,total:17276, pass:0, block:17276
    + * 1529399835826,total:18722, pass:0, block:18722
    + * 1529399836826,total:19490, pass:0, block:19492
    + * 1529399837828,total:19355, pass:0, block:19355
    + * 1529399838827,total:11388, pass:0, block:11388
    + * 1529399839829,total:14494, pass:104, block:14390 //After 10 seconds, the system is restored, and degraded very
    + * quickly
    + * 1529399840854,total:18505, pass:0, block:18505
    + * 1529399841854,total:19673, pass:0, block:19676
    + * 
    + * + * @author jialiang.linjl + */ +public class RtDegradeDemo { + + private static final String KEY = "abc"; + + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger total = new AtomicInteger(); + + private static volatile boolean stop = false; + private static final int threadCount = 100; + private static int seconds = 60 + 40; + + public static void main(String[] args) throws Exception { + + tick(); + initDegradeRule(); + + for (int i = 0; i < threadCount; i++) { + Thread entryThread = new Thread(new Runnable() { + + @Override + public void run() { + while (true) { + Entry entry = null; + try { + TimeUnit.MILLISECONDS.sleep(5); + entry = SphU.entry(KEY); + // token acquired + pass.incrementAndGet(); + // sleep 600 ms, as rt + TimeUnit.MILLISECONDS.sleep(600); + } catch (Exception e) { + block.incrementAndGet(); + } finally { + total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + } + } + + }); + entryThread.setName("working-thread"); + entryThread.start(); + } + } + + private static void initDegradeRule() { + List rules = new ArrayList(); + DegradeRule rule = new DegradeRule(); + rule.setResource(KEY); + // set threshold rt, 10 ms + rule.setCount(10); + rule.setGrade(RuleConstant.DEGRADE_GRADE_RT); + rule.setTimeWindow(10); + rules.add(rule); + DegradeRuleManager.loadRules(rules); + } + + private static void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + static class TimerTask implements Runnable { + + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("begin to statistic!!!"); + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + + ", pass:" + oneSecondPass + ", block:" + oneSecondBlock); + + if (seconds-- <= 0) { + stop = true; + } + } + + long cost = System.currentTimeMillis() - start; + System.out.println("time cost: " + cost + " ms"); + System.out.println("total:" + total.get() + ", pass:" + pass.get() + + ", block:" + block.get()); + System.exit(0); + } + } + +} diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowQpsDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowQpsDemo.java new file mode 100755 index 00000000..7f5114ac --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowQpsDemo.java @@ -0,0 +1,161 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +/** + * @author jialiang.linjl + */ +public class FlowQpsDemo { + + private static final String KEY = "abc"; + + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger total = new AtomicInteger(); + + private static volatile boolean stop = false; + + private static final int threadCount = 1; + + private static int seconds = 60 + 40; + + public static void main(String[] args) throws Exception { + tick(); + // first make the system run on a very low condition + simulateTraffic(); + + System.out.println("===== begin to do flow control"); + System.out.println("only 20 requests per second can pass"); + initFlowQpsRule(); + + } + + private static void initFlowQpsRule() { + List rules = new ArrayList(); + FlowRule rule1 = new FlowRule(); + rule1.setResource(KEY); + // set limit qps to 20 + rule1.setCount(20); + rule1.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule1.setLimitApp("default"); + rules.add(rule1); + FlowRuleManager.loadRules(rules); + } + + private static void simulateTraffic() { + for (int i = 0; i < threadCount; i++) { + Thread t = new Thread(new RunTask()); + t.setName("simulate-traffic-Task"); + t.start(); + } + } + + private static void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + static class TimerTask implements Runnable { + + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("begin to statistic!!!"); + + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + System.out.println(seconds + " send qps is: " + oneSecondTotal); + System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + + ", pass:" + oneSecondPass + + ", block:" + oneSecondBlock); + + if (seconds-- <= 0) { + stop = true; + } + } + + long cost = System.currentTimeMillis() - start; + System.out.println("time cost: " + cost + " ms"); + System.out.println("total:" + total.get() + ", pass:" + pass.get() + + ", block:" + block.get()); + System.exit(0); + } + } + + static class RunTask implements Runnable { + @Override + public void run() { + while (!stop) { + Entry entry = null; + + try { + entry = SphU.entry(KEY); + // token acquired, means pass + pass.addAndGet(1); + } catch (BlockException e1) { + block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + + Random random2 = new Random(); + try { + TimeUnit.MILLISECONDS.sleep(random2.nextInt(50)); + } catch (InterruptedException e) { + // ignore + } + } + } + } +} diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowThreadDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowThreadDemo.java new file mode 100755 index 00000000..4b55f178 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/FlowThreadDemo.java @@ -0,0 +1,155 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +/** + * @author jialiang.linjl + */ +public class FlowThreadDemo { + + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger total = new AtomicInteger(); + private static AtomicInteger activeThread = new AtomicInteger(); + + private static volatile boolean stop = false; + private static final int threadCount = 100; + + private static int seconds = 60 + 40; + private static volatile int methodBRunningTime = 2000; + + public static void main(String[] args) throws Exception { + System.out.println( + "MethodA will call methodB. After running for a while, methodB becomes slow, " + + "which make methodA also becomes slow "); + tick(); + initFlowRule(); + + for (int i = 0; i < threadCount; i++) { + Thread entryThread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + Entry methodA = null; + try { + TimeUnit.MILLISECONDS.sleep(5); + methodA = SphU.entry("methodA"); + activeThread.incrementAndGet(); + Entry methodB = SphU.entry("methodB"); + TimeUnit.MILLISECONDS.sleep(methodBRunningTime); + methodB.exit(); + pass.addAndGet(1); + } catch (BlockException e1) { + block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + total.incrementAndGet(); + if (methodA != null) { + methodA.exit(); + activeThread.decrementAndGet(); + } + } + } + } + }); + entryThread.setName("working thread"); + entryThread.start(); + } + } + + private static void initFlowRule() { + List rules = new ArrayList(); + FlowRule rule1 = new FlowRule(); + rule1.setResource("methodA"); + // set limit concurrent thread for 'methodA' to 20 + rule1.setCount(20); + rule1.setGrade(RuleConstant.FLOW_GRADE_THREAD); + rule1.setLimitApp("default"); + + rules.add(rule1); + FlowRuleManager.loadRules(rules); + } + + private static void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + static class TimerTask implements Runnable { + + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("begin to statistic!!!"); + + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + System.out.println(seconds + " total qps is: " + oneSecondTotal); + System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + + ", pass:" + oneSecondPass + + ", block:" + oneSecondBlock + + " activeThread:" + activeThread.get()); + if (seconds-- <= 0) { + stop = true; + } + if (seconds == 40) { + System.out.println("method B is running much faster; more requests are allowed to pass"); + methodBRunningTime = 20; + } + } + + long cost = System.currentTimeMillis() - start; + System.out.println("time cost: " + cost + " ms"); + System.out.println("total:" + total.get() + ", pass:" + pass.get() + + ", block:" + block.get()); + System.exit(0); + } + } +} diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/PaceFlowDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/PaceFlowDemo.java new file mode 100755 index 00000000..cd9e026f --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/PaceFlowDemo.java @@ -0,0 +1,204 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +/** + *

    + * If {@link RuleConstant#CONTROL_BEHAVIOR_RATE_LIMITER} is set, incoming + * requests are passing at regular interval. When a new request arrives, the + * flow rule checks whether the interval between the new request and the + * previous request. If the interval is less than the count set in the rule + * first. If the interval is large, it will pass the request; otherwise, + * sentinel will calculate the waiting time for this request. If the waiting + * time is longer than the {@link FlowRule#maxQueueingTimeMs} set in the rule, + * the request will be rejected immediately. + * + * This method is widely used for pulsed flow. When a large amount of flow + * comes, we don't want to pass all these requests at once, which may drag the + * system down. We can make the system handle these requests at a steady pace by + * using this kind of rules. + * + *

    + * This demo demonstrates how to use {@link RuleConstant#CONTROL_BEHAVIOR_RATE_LIMITER}. + *

    + * + *

    + * {@link #initPaceFlowRule() } create rules that uses + * {@code CONTROL_BEHAVIOR_RATE_LIMITER}. + *

    + * {@link #simulatePulseFlow()} simulates 100 requests that arrives at almost the + * same time. All these 100 request are passed at a fixed interval. + * + *

    + * Run this demo, results are as follows: + *

    + * pace behavior
    + * ....
    + * 1528872403887 one request pass, cost 9348 ms // every 100 ms pass one request.
    + * 1528872403986 one request pass, cost 9469 ms
    + * 1528872404087 one request pass, cost 9570 ms
    + * 1528872404187 one request pass, cost 9642 ms
    + * 1528872404287 one request pass, cost 9770 ms
    + * 1528872404387 one request pass, cost 9848 ms
    + * 1528872404487 one request pass, cost 9970 ms
    + * ...
    + * done
    + * total pass:100, total block:0
    + * 
    + * + * Then we invoke {@link #initDefaultFlowRule()} to set rules with default behavior, and only 10 + * requests will be allowed to pass, other requests will be rejected immediately. + *

    + * The output will be like: + *

    + * default behavior
    + * 1530500101279 one request pass, cost 0 ms
    + * 1530500101279 one request pass, cost 0 ms
    + * 1530500101279 one request pass, cost 0 ms
    + * 1530500101279 one request pass, cost 0 ms
    + * 1530500101279 one request pass, cost 0 ms
    + * 1530500101279 one request pass, cost 0 ms
    + * 1530500101280 one request pass, cost 1 ms
    + * 1530500101280 one request pass, cost 0 ms
    + * 1530500101280 one request pass, cost 0 ms
    + * 1530500101280 one request pass, cost 0 ms
    + * done
    + * total pass:10, total block:90 // 10 requests passed, other 90 requests rejected immediately.
    + * 
    + * + * @author jialiang.linjl + */ +public class PaceFlowDemo { + + private static final String KEY = "abc"; + + private static volatile CountDownLatch countDown; + + private static final Integer requestQps = 100; + private static final Integer count = 10; + private static final AtomicInteger done = new AtomicInteger(); + private static final AtomicInteger pass = new AtomicInteger(); + private static final AtomicInteger block = new AtomicInteger(); + + public static void main(String[] args) throws InterruptedException { + System.out.println("pace behavior"); + countDown = new CountDownLatch(1); + initPaceFlowRule(); + simulatePulseFlow(); + countDown.await(); + + System.out.println("done"); + System.out.println("total pass:" + pass.get() + ", total block:" + block.get()); + + System.out.println(); + System.out.println("default behavior"); + TimeUnit.SECONDS.sleep(5); + done.set(0); + pass.set(0); + block.set(0); + countDown = new CountDownLatch(1); + initDefaultFlowRule(); + simulatePulseFlow(); + countDown.await(); + System.out.println("done"); + System.out.println("total pass:" + pass.get() + ", total block:" + block.get()); + System.exit(0); + } + + private static void initPaceFlowRule() { + List rules = new ArrayList(); + FlowRule rule1 = new FlowRule(); + rule1.setResource(KEY); + rule1.setCount(count); + rule1.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule1.setLimitApp("default"); + /* + * CONTROL_BEHAVIOR_RATE_LIMITER means requests more than threshold will be queueing in the queue, + * until the queueing time is more than {@link FlowRule#maxQueueingTimeMs}, the requests will be rejected. + */ + rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); + rule1.setMaxQueueingTimeMs(20 * 1000); + + rules.add(rule1); + FlowRuleManager.loadRules(rules); + } + + private static void initDefaultFlowRule() { + List rules = new ArrayList(); + FlowRule rule1 = new FlowRule(); + rule1.setResource(KEY); + rule1.setCount(count); + rule1.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule1.setLimitApp("default"); + // CONTROL_BEHAVIOR_DEFAULT means requests more than threshold will be rejected immediately. + rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); + + rules.add(rule1); + FlowRuleManager.loadRules(rules); + } + + private static void simulatePulseFlow() { + for (int i = 0; i < requestQps; i++) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + long startTime = TimeUtil.currentTimeMillis(); + Entry entry = null; + try { + entry = SphU.entry(KEY); + } catch (BlockException e1) { + block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + if (entry != null) { + entry.exit(); + pass.incrementAndGet(); + long cost = TimeUtil.currentTimeMillis() - startTime; + System.out.println( + TimeUtil.currentTimeMillis() + " one request pass, cost " + cost + " ms"); + } + } + + try { + TimeUnit.MILLISECONDS.sleep(5); + } catch (InterruptedException e1) { + // ignore + } + + if (done.incrementAndGet() >= requestQps) { + countDown.countDown(); + } + } + }, "Thread " + i); + thread.start(); + } + } +} diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/WarmUpFlowDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/WarmUpFlowDemo.java new file mode 100755 index 00000000..b2027e61 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/flow/WarmUpFlowDemo.java @@ -0,0 +1,224 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +/** + * When {@link FlowRule#controlBehavior} set to {@link RuleConstant#CONTROL_BEHAVIOR_WARM_UP}, real passed qps will + * gradually increase to {@link FlowRule#count}, other than burst increasing. + *

    + * Run this demo, results are as follows: + *

    + * ...
    + * 1530497805902, total:1, pass:1, block:0 // run in slow qps
    + * 1530497806905, total:3, pass:3, block:0
    + * 1530497807909, total:2, pass:2, block:0
    + * 1530497808913, total:3, pass:3, block:0
    + * 1530497809917, total:269, pass:6, block:263 // request qps burst increase, warm up behavior triggered.
    + * 1530497810917, total:3676, pass:7, block:3669
    + * 1530497811919, total:3734, pass:9, block:3725
    + * 1530497812920, total:3692, pass:9, block:3683
    + * 1530497813923, total:3642, pass:10, block:3632
    + * 1530497814926, total:3685, pass:10, block:3675
    + * 1530497815930, total:3671, pass:11, block:3660
    + * 1530497816933, total:3660, pass:15, block:3645
    + * 1530497817936, total:3681, pass:21, block:3661 // warm up process end, pass qps increased to {@link FlowRule#count}
    + * 1530497818940, total:3737, pass:20, block:3716
    + * 1530497819945, total:3663, pass:20, block:3643
    + * 1530497820950, total:3723, pass:21, block:3702
    + * 1530497821954, total:3680, pass:20, block:3660
    + * ...
    + * 
    + * + * @author jialiang.linjl + */ +public class WarmUpFlowDemo { + + private static final String KEY = "abc"; + + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger total = new AtomicInteger(); + + private static volatile boolean stop = false; + + private static final int threadCount = 100; + private static int seconds = 60 + 40; + + public static void main(String[] args) throws Exception { + initFlowRule(); + // trigger Sentinel internal init + Entry entry = null; + try { + entry = SphU.entry(KEY); + } catch (Exception e) { + } finally { + if (entry != null) { + entry.exit(); + } + } + + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + + //first make the system run on a very low condition + for (int i = 0; i < 3; i++) { + Thread t = new Thread(new WarmUpTask()); + t.setName("sentinel-warmup-task"); + t.start(); + } + Thread.sleep(20000); + + /* + * Start more thread to simulate more qps. Since we use {@link RuleConstant.CONTROL_BEHAVIOR_WARM_UP} as + * {@link FlowRule#controlBehavior}, real passed qps will increase to {@link FlowRule#count} in + * {@link FlowRule#warmUpPeriodSec} seconds. + */ + for (int i = 0; i < threadCount; i++) { + Thread t = new Thread(new RunTask()); + t.setName("sentinel-run-task"); + t.start(); + } + } + + private static void initFlowRule() { + List rules = new ArrayList(); + FlowRule rule1 = new FlowRule(); + rule1.setResource(KEY); + rule1.setCount(20); + rule1.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule1.setLimitApp("default"); + rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); + rule1.setWarmUpPeriodSec(10); + + rules.add(rule1); + FlowRuleManager.loadRules(rules); + } + + static class WarmUpTask implements Runnable { + @Override + public void run() { + while (!stop) { + Entry entry = null; + try { + entry = SphU.entry(KEY); + // token acquired, means pass + pass.addAndGet(1); + } catch (BlockException e1) { + block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + Random random2 = new Random(); + try { + TimeUnit.MILLISECONDS.sleep(random2.nextInt(2000)); + } catch (InterruptedException e) { + // ignore + } + } + } + } + + static class RunTask implements Runnable { + @Override + public void run() { + while (!stop) { + Entry entry = null; + try { + entry = SphU.entry(KEY); + pass.addAndGet(1); + } catch (BlockException e1) { + block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + Random random2 = new Random(); + try { + TimeUnit.MILLISECONDS.sleep(random2.nextInt(50)); + } catch (InterruptedException e) { + // ignore + } + } + } + } + + static class TimerTask implements Runnable { + + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("begin to statistic!!!"); + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + + ", pass:" + oneSecondPass + + ", block:" + oneSecondBlock); + if (seconds-- <= 0) { + stop = true; + } + } + + long cost = System.currentTimeMillis() - start; + System.out.println("time cost: " + cost + " ms"); + System.out.println("total:" + total.get() + ", pass:" + pass.get() + + ", block:" + block.get()); + System.exit(0); + } + } +} diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/system/SystemGuardDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/system/SystemGuardDemo.java new file mode 100755 index 00000000..ac8706c4 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/system/SystemGuardDemo.java @@ -0,0 +1,145 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.system; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.util.TimeUtil; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; + +/** + * @author jialiang.linjl + */ +public class SystemGuardDemo { + + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger total = new AtomicInteger(); + + private static volatile boolean stop = false; + private static final int threadCount = 100; + + private static int seconds = 60 + 40; + + public static void main(String[] args) throws Exception { + + tick(); + initSystemRule(); + + for (int i = 0; i < threadCount; i++) { + Thread entryThread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + Entry entry = null; + try { + entry = SphU.entry("methodA", EntryType.IN); + pass.incrementAndGet(); + try { + TimeUnit.MILLISECONDS.sleep(20); + } catch (InterruptedException e) { + // ignore + } + } catch (BlockException e1) { + block.incrementAndGet(); + try { + TimeUnit.MILLISECONDS.sleep(20); + } catch (InterruptedException e) { + // ignore + } + } catch (Exception e2) { + // biz exception + } finally { + total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + } + } + + }); + entryThread.setName("working-thread"); + entryThread.start(); + } + } + + private static void initSystemRule() { + List rules = new ArrayList(); + SystemRule rule = new SystemRule(); + // max load is 3 + rule.setHighestSystemLoad(3.0); + // max avg rt of all request is 10 ms + rule.setAvgRt(10); + // max total qps is 20 + rule.setQps(20); + // max parallel working thread is 10 + rule.setMaxThread(10); + + rules.add(rule); + SystemRuleManager.loadRules(Collections.singletonList(rule)); + } + + private static void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + static class TimerTask implements Runnable { + @Override + public void run() { + System.out.println("begin to statistic!!!"); + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + System.out.println(seconds + ", " + TimeUtil.currentTimeMillis() + ", total:" + + oneSecondTotal + ", pass:" + + oneSecondPass + ", block:" + oneSecondBlock); + if (seconds-- <= 0) { + stop = true; + } + } + System.exit(0); + } + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/README.md b/sentinel-demo/sentinel-demo-dubbo/README.md new file mode 100644 index 00000000..d929c996 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/README.md @@ -0,0 +1,78 @@ +# Sentinel Dubbo Demo + +Sentinel 提供了与 Dubbo 整合的模块 - Sentinel Dubbo Adapter,主要包括针对 Service Provider 和 Service Consumer 实现的 Filter。使用时用户只需引入以下模块(以 Maven 为例): + +```xml + + com.alibaba.csp + sentinel-dubbo-adapter + x.y.z + +``` + +引入此依赖后,Dubbo 的服务接口和方法(包括调用端和服务端)就会成为 Sentinel 中的资源,在配置了规则后就可以自动享受到 Sentinel 的防护能力。 +若不希望开启 Sentinel Dubbo Adapter 中的某个 Filter,可以手动关闭对应的 Filter,比如: + +```java +@Bean +public ConsumerConfig consumerConfig() { + ConsumerConfig consumerConfig = new ConsumerConfig(); + consumerConfig.setFilter("-sentinel.dubbo.consumer.filter"); + return consumerConfig; +} +``` + +我们提供了几个具体的 Demo 来分别演示 Provider 和 Consumer 的限流场景。 + +## Service Provider + +Service Provider 用于向外界提供服务,处理各个消费者的调用请求。为了保护 Provider 不被激增的流量拖垮影响稳定性,可以给 Provider 配置 **QPS 模式**的限流,这样当每秒的请求量超过设定的阈值时会自动拒绝多的请求。限流粒度可以是服务接口和服务方法两种粒度。若希望整个服务接口的 QPS 不超过一定数值,则可以为对应服务接口资源(resourceName 为**接口全限定名**)配置 QPS 阈值;若希望服务的某个方法的 QPS 不超过一定数值,则可以为对应服务方法资源(resourceName 为**接口全限定名:方法签名**)配置 QPS 阈值。有关配置详情请参考 [流量控制 | Sentinel](https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6)。 + +Demo 1 演示了此限流场景,我们看一下这种模式的限流产生的效果。假设我们已经定义了某个服务接口 `com.alibaba.csp.sentinel.demo.dubbo.FooService`,其中有一个方法 `sayHello(java.lang.String)`,Provider 端该方法设定 QPS 阈值为 10。在 Consumer 端在 1s 之内连续发起 15 次调用,可以通过日志文件看到 Provider 端被限流。拦截日志统一记录在 `~/logs/csp/sentinel-block.log` 中: + +``` +2018-07-24 17:13:43|1|com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String),FlowException,default,|5,0 +``` + +在 Provider 对应的 metrics 日志中也有记录: + +``` +1532423623000|2018-07-24 17:13:43|com.alibaba.csp.sentinel.demo.dubbo.FooService|15|0|15|0|3 +1532423623000|2018-07-24 17:13:43|com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)|10|5|10|0|0 +``` + +很多场景下,根据**调用方**来限流也是非常重要的。比如有两个服务 A 和 B 都向 Service Provider 发起调用请求,我们希望只对来自服务 B 的请求进行限流,则可以设置限流规则的 `limitApp` 为服务 B 的名称。Sentinel Dubbo Adapter 会自动解析 Dubbo 消费者(调用方)的 application name 作为调用方名称(`origin`),在进行资源保护的时候都会带上调用方名称。若限流规则未配置调用方(`default`),则该限流规则对所有调用方生效。若限流规则配置了调用方则限流规则将仅对指定调用方生效。 + +> 注:Dubbo 默认通信不携带对端 application name 信息,因此需要开发者在调用端手动将 application name 置入 attachment 中,provider 端进行相应的解析。Sentinel Dubbo Adapter 实现了一个 Filter 用于自动从 consumer 端向 provider 端透传 application name。若调用端未引入 Sentinel Dubbo Adapter,又希望根据调用端限流,可以在调用端手动将 application name 置入 attachment 中,key 为 `dubboApplication`。 + +在限流日志中会也会记录调用方的名称,如: + +``` +2018-07-25 16:26:48|1|com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String),FlowException,default,demo-consumer|5,0 +``` + +其中日志中的 `demo-consumer` 即为调用方名称。 + +## Service Consumer + +Service Consumer 作为客户端去调用远程服务。每一个服务都可能会依赖几个下游服务,若某个服务 A 依赖的下游服务 B 出现了不稳定的情况,服务 A 请求 服务 B 的响应时间变长,从而服务 A 调用服务 B 的线程就会产生堆积,最终可能耗尽服务 A 的线程数。我们通过用并发线程数来控制对下游服务 B 的访问,来保证下游服务不可靠的时候,不会拖垮服务自身。基于这种场景,推荐给 Consumer 配置**线程数模式**的限流,来保证自身不被不稳定服务所影响。限流粒度同样可以是服务接口和服务方法两种粒度。 + +采用基于线程数的限流模式后,我们不需要再显式地去进行线程池隔离,Sentinel 会控制资源的线程数,超出的请求直接拒绝,直到堆积的线程处理完成。 + +Demo 2 演示了此限流场景,我们看一下这种模式的效果。假设当前服务 A 依赖两个远程服务方法 `sayHello(java.lang.String)` 和 `doAnother()`。前者远程调用的响应时间 为 1s-1.5s之间,后者 RT 非常小(30 ms 左右)。服务 A 端设两个远程方法 thread count 为 5。然后每隔 50 ms 左右向线程池投入两个任务,作为消费者分别远程调用对应方法,持续 10 次。可以看到 `sayHello` 方法被限流 5 次,因为后面调用的时候前面的远程调用还未返回(RT 高);而 `doAnother()` 调用则不受影响。线程数目超出时快速失败能够有效地防止自己被慢调用所影响。 + +## Sentinel Dashboard + +Sentinel 还提供 API 用于获取实时的监控信息,对应文档见[此处](https://github.com/alibaba/Sentinel/wiki/%E5%AE%9E%E6%97%B6%E7%9B%91%E6%8E%A7)。为了便于使用,Sentinel 还提供了一个控制台(Dashboard)用于配置规则、查看监控、机器发现等功能。我们只需要按照 [Sentinel 控制台文档](https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0) 启动控制台,然后给对应的应用程序添加相应参数并启动即可。比如本文中 Service Provider 示例的启动参数: + +```bash +-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.api.port=8720 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=dubbo-provider-demo +``` + +这样在启动 Service Provider 示例以后,就可以在 Sentinel 控制台中找到我们的服务了。可以很方便地在控制台中配置限流规则: + +![规则配置](http://dubbo.incubator.apache.org/img/blog/sentinel-dashboard-view-rules.png) + +或者查看实时监控数据: + +![秒级实时监控](http://dubbo.incubator.apache.org/img/blog/sentinel-dashboard-metrics.png) \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-dubbo/pom.xml b/sentinel-demo/sentinel-demo-dubbo/pom.xml new file mode 100644 index 00000000..727ad84d --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/pom.xml @@ -0,0 +1,51 @@ + + + + sentinel-demo + com.alibaba.csp + 0.1.0 + + 4.0.0 + + sentinel-demo-dubbo + + + + org.springframework + spring-context + 5.0.7.RELEASE + + + com.alibaba + dubbo + 2.6.2 + + + com.alibaba.csp + sentinel-dubbo-adapter + ${project.version} + + + com.alibaba.csp + sentinel-transport-simple-http + ${project.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + 1.8 + 1.8 + ${java.encoding} + + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/FooService.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/FooService.java new file mode 100644 index 00000000..58c2bfd0 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/FooService.java @@ -0,0 +1,25 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo; + +/** + * @author Eric Zhao + */ +public interface FooService { + String sayHello(String name); + + String doAnother(); +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/ConsumerConfiguration.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/ConsumerConfiguration.java new file mode 100644 index 00000000..f6297a95 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/ConsumerConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.consumer; + +import com.alibaba.dubbo.config.ApplicationConfig; +import com.alibaba.dubbo.config.ConsumerConfig; +import com.alibaba.dubbo.config.RegistryConfig; +import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Eric Zhao + */ +@Configuration +@DubboComponentScan +public class ConsumerConfiguration { + @Bean + public ApplicationConfig applicationConfig() { + ApplicationConfig applicationConfig = new ApplicationConfig(); + applicationConfig.setName("demo-consumer"); + return applicationConfig; + } + + @Bean + public RegistryConfig registryConfig() { + RegistryConfig registryConfig = new RegistryConfig(); + registryConfig.setAddress("multicast://224.5.6.7:1234"); + return registryConfig; + } + + @Bean + public ConsumerConfig consumerConfig() { + ConsumerConfig consumerConfig = new ConsumerConfig(); + // Uncomment below line if you don't want to enable Sentinel for Dubbo service consumers. + // consumerConfig.setFilter("-sentinel.dubbo.consumer.filter"); + return consumerConfig; + } + + @Bean + public FooServiceConsumer annotationDemoServiceConsumer() { + return new FooServiceConsumer(); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/FooServiceConsumer.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/FooServiceConsumer.java new file mode 100644 index 00000000..199f21d1 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/consumer/FooServiceConsumer.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.consumer; + +import com.alibaba.csp.sentinel.demo.dubbo.FooService; +import com.alibaba.dubbo.config.annotation.Reference; + +/** + * @author Eric Zhao + */ +public class FooServiceConsumer { + + @Reference(url = "dubbo://127.0.0.1:25758", timeout = 3000) + private FooService fooService; + + public String sayHello(String name) { + return fooService.sayHello(name); + } + + public String doAnother() { + return fooService.doAnother(); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooConsumerBootstrap.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooConsumerBootstrap.java new file mode 100644 index 00000000..949f7a83 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooConsumerBootstrap.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo1; + + +import com.alibaba.csp.sentinel.demo.dubbo.consumer.ConsumerConfiguration; +import com.alibaba.csp.sentinel.demo.dubbo.consumer.FooServiceConsumer; +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * Please add the following VM arguments: + *
    + * -Djava.net.preferIPv4Stack=true
    + * -Dcsp.sentinel.api.port=8721
    + * -Dproject.name=dubbo-consumer-demo
    + * 
    + * + * @author Eric Zhao + */ +public class FooConsumerBootstrap { + + public static void main(String[] args) { + InitExecutor.doInit(); + + AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext(); + consumerContext.register(ConsumerConfiguration.class); + consumerContext.refresh(); + + FooServiceConsumer service = consumerContext.getBean(FooServiceConsumer.class); + + for (int i = 0; i < 15; i++) { + try { + String message = service.sayHello("Eric"); + System.out.println("Success: " + message); + } catch (SentinelRpcException ex) { + System.out.println("Blocked"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooProviderBootstrap.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooProviderBootstrap.java new file mode 100644 index 00000000..ceafae51 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooProviderBootstrap.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo1; + +import java.util.Collections; + +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * Please add the following VM arguments: + *
    + * -Djava.net.preferIPv4Stack=true
    + * -Dcsp.sentinel.api.port=8720
    + * -Dproject.name=dubbo-provider-demo
    + * 
    + * + * @author Eric Zhao + */ +public class FooProviderBootstrap { + + private static final String RES_KEY = "com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)"; + private static final String INTERFACE_RES_KEY = "com.alibaba.csp.sentinel.demo.dubbo.FooService"; + + public static void main(String[] args) { + initFlowRule(); + InitExecutor.doInit(); + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ProviderConfiguration.class); + context.refresh(); + + System.out.println("Service provider is ready"); + } + + private static void initFlowRule() { + FlowRule flowRule = new FlowRule(); + flowRule.setResource(RES_KEY); + flowRule.setCount(10); + flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); + flowRule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(flowRule)); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooServiceImpl.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooServiceImpl.java new file mode 100644 index 00000000..03065f80 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/FooServiceImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo1; + +import java.time.LocalDateTime; + +import com.alibaba.csp.sentinel.demo.dubbo.FooService; +import com.alibaba.dubbo.config.annotation.Service; + +/** + * @author Eric Zhao + */ +@Service +public class FooServiceImpl implements FooService { + + @Override + public String sayHello(String name) { + return String.format("Hello, %s at %s", name, LocalDateTime.now()); + } + + @Override + public String doAnother() { + return LocalDateTime.now().toString(); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/ProviderConfiguration.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/ProviderConfiguration.java new file mode 100644 index 00000000..9f73c231 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo1/ProviderConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo1; + +import com.alibaba.dubbo.config.ApplicationConfig; +import com.alibaba.dubbo.config.ProtocolConfig; +import com.alibaba.dubbo.config.RegistryConfig; +import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Eric Zhao + */ +@Configuration +@DubboComponentScan("com.alibaba.csp.sentinel.demo.dubbo.demo1") +public class ProviderConfiguration { + + @Bean + public ApplicationConfig applicationConfig() { + ApplicationConfig applicationConfig = new ApplicationConfig(); + applicationConfig.setName("demo-provider"); + return applicationConfig; + } + + @Bean + public RegistryConfig registryConfig() { + RegistryConfig registryConfig = new RegistryConfig(); + registryConfig.setAddress("multicast://224.5.6.7:1234"); + return registryConfig; + } + + @Bean + public ProtocolConfig protocolConfig() { + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.setName("dubbo"); + protocolConfig.setPort(25758); + return protocolConfig; + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooConsumerBootstrap.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooConsumerBootstrap.java new file mode 100644 index 00000000..b4ab226d --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooConsumerBootstrap.java @@ -0,0 +1,85 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo2; + +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; +import com.alibaba.csp.sentinel.demo.dubbo.consumer.ConsumerConfiguration; +import com.alibaba.csp.sentinel.demo.dubbo.consumer.FooServiceConsumer; +import com.alibaba.csp.sentinel.init.InitExecutor; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.SentinelRpcException; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * Please add the following VM arguments: + *
    + * -Djava.net.preferIPv4Stack=true
    + * -Dcsp.sentinel.api.port=8721
    + * -Dproject.name=dubbo-consumer-demo
    + * 
    + * + * @author Eric Zhao + */ +public class FooConsumerBootstrap { + + private static final String RES_KEY = "com.alibaba.csp.sentinel.demo.dubbo.FooService:sayHello(java.lang.String)"; + private static final String INTERFACE_RES_KEY = "com.alibaba.csp.sentinel.demo.dubbo.FooService"; + + private static final ExecutorService pool = Executors.newFixedThreadPool(10, + new NamedThreadFactory("dubbo-consumer-pool")); + + public static void main(String[] args) { + initFlowRule(); + InitExecutor.doInit(); + + AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext(); + consumerContext.register(ConsumerConfiguration.class); + consumerContext.refresh(); + + FooServiceConsumer service = consumerContext.getBean(FooServiceConsumer.class); + for (int i = 0; i < 10; i++) { + pool.submit(() -> { + try { + String message = service.sayHello("Eric"); + System.out.println("Success: " + message); + } catch (SentinelRpcException ex) { + System.out.println("Blocked"); + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + pool.submit(() -> { + System.out.println("Another: " + service.doAnother()); + }); + } + } + + private static void initFlowRule() { + FlowRule flowRule = new FlowRule(); + flowRule.setResource(RES_KEY); + flowRule.setCount(5); + flowRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); + flowRule.setLimitApp("default"); + FlowRuleManager.loadRules(Collections.singletonList(flowRule)); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooProviderBootstrap.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooProviderBootstrap.java new file mode 100644 index 00000000..513b0d0d --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooProviderBootstrap.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo2; + +import com.alibaba.csp.sentinel.init.InitExecutor; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * Please add the following VM arguments: + *
    + * -Djava.net.preferIPv4Stack=true
    + * -Dcsp.sentinel.api.port=8720
    + * -Dproject.name=dubbo-provider-demo
    + * 
    + * + * @author Eric Zhao + */ +public class FooProviderBootstrap { + + public static void main(String[] args) { + InitExecutor.doInit(); + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ProviderConfiguration.class); + context.refresh(); + + System.out.println("Service provider is ready"); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooServiceImpl.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooServiceImpl.java new file mode 100644 index 00000000..9ecf8d8a --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/FooServiceImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo2; + +import java.time.LocalDateTime; + +import com.alibaba.csp.sentinel.demo.dubbo.FooService; +import com.alibaba.dubbo.config.annotation.Service; + +/** + * @author Eric Zhao + */ +@Service +public class FooServiceImpl implements FooService { + + @Override + public String sayHello(String name) { + return String.format("Hello, %s at %s", name, LocalDateTime.now()); + } + + @Override + public String doAnother() { + return LocalDateTime.now().toString(); + } +} diff --git a/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/ProviderConfiguration.java b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/ProviderConfiguration.java new file mode 100644 index 00000000..aed473f5 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dubbo/src/main/java/com/alibaba/csp/sentinel/demo/dubbo/demo2/ProviderConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.dubbo.demo2; + +import com.alibaba.dubbo.config.ApplicationConfig; +import com.alibaba.dubbo.config.ProtocolConfig; +import com.alibaba.dubbo.config.RegistryConfig; +import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Eric Zhao + */ +@Configuration +@DubboComponentScan("com.alibaba.csp.sentinel.demo.dubbo.demo2") +public class ProviderConfiguration { + + @Bean + public ApplicationConfig applicationConfig() { + ApplicationConfig applicationConfig = new ApplicationConfig(); + applicationConfig.setName("demo-provider"); + return applicationConfig; + } + + @Bean + public RegistryConfig registryConfig() { + RegistryConfig registryConfig = new RegistryConfig(); + registryConfig.setAddress("multicast://224.5.6.7:1234"); + return registryConfig; + } + + @Bean + public ProtocolConfig protocolConfig() { + ProtocolConfig protocolConfig = new ProtocolConfig(); + protocolConfig.setName("dubbo"); + protocolConfig.setPort(25758); + return protocolConfig; + } +} diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml b/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml new file mode 100755 index 00000000..127220cd --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-demo + 0.1.0 + + sentinel-demo-dynamic-file-rule + + + + com.alibaba.csp + sentinel-core + + + com.alibaba.csp + sentinel-datasource-extension + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceDemo.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceDemo.java new file mode 100644 index 00000000..3bd632ab --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceDemo.java @@ -0,0 +1,96 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.file.rule; + +import java.util.List; + +import com.alibaba.csp.sentinel.datasource.ConfigParser; +import com.alibaba.csp.sentinel.datasource.DataSource; +import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource; +import com.alibaba.csp.sentinel.demo.file.rule.parser.JsonDegradeRuleListParser; +import com.alibaba.csp.sentinel.demo.file.rule.parser.JsonFlowRuleListParser; +import com.alibaba.csp.sentinel.demo.file.rule.parser.JsonSystemRuleListParser; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; + +/** + *

    + * This Demo shows how to use {@link FileRefreshableDataSource} to read {@link Rule}s from file. The + * {@link FileRefreshableDataSource} will automatically fetches the backend file every 3 seconds, and + * inform the listener if the file is updated. + *

    + *

    + * Each {@link DataSource} has a {@link SentinelProperty} to hold the deserialized config data. + * {@link PropertyListener} will listen to the {@link SentinelProperty} instead of the datasource. + * {@link ConfigParser} is used for telling how to deserialize the data. + *

    + *

    + * {@link FlowRuleManager#register2Property(SentinelProperty)}, + * {@link DegradeRuleManager#register2Property(SentinelProperty)}, + * {@link SystemRuleManager#register2Property(SentinelProperty)} could be called for listening the + * {@link Rule}s change. + *

    + *

    + * For other kinds of data source, such as Nacos, + * Zookeeper, Git, or even CSV file, We could implement {@link DataSource} interface to read these + * configs. + *

    + * + * @author Carpenter Lee + */ +public class FileDataSourceDemo { + + public static void main(String[] args) throws Exception { + FileDataSourceDemo demo = new FileDataSourceDemo(); + demo.listenRules(); + + /** + * Start to require tokens, rate will be limited by rule in FlowRule.json + */ + FlowQpsRunner runner = new FlowQpsRunner(); + runner.simulateTraffic(); + runner.tick(); + } + + public void listenRules() throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + String flowRulePath = classLoader.getResource("FlowRule.json").getFile(); + String degradeRulePath = classLoader.getResource("DegradeRule.json").getFile(); + String systemRulePath = classLoader.getResource("SystemRule.json").getFile(); + + // data source for FlowRule + DataSource> flowRuleDataSource = new FileRefreshableDataSource>( + flowRulePath, new JsonFlowRuleListParser()); + FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); + + // data source for DegradeRule + DataSource> degradeRuleDataSource = new FileRefreshableDataSource>( + degradeRulePath, new JsonDegradeRuleListParser()); + DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty()); + + // data source for SystemRule + DataSource> systemRuleDataSource = new FileRefreshableDataSource>( + systemRulePath, new JsonSystemRuleListParser()); + SystemRuleManager.register2Property(systemRuleDataSource.getProperty()); + } +} diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FlowQpsRunner.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FlowQpsRunner.java new file mode 100644 index 00000000..e9baaadf --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FlowQpsRunner.java @@ -0,0 +1,132 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.file.rule; + +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.TimeUtil; + +/** + * Flow Rule demo. + * + * @author Carpenter Lee + */ +class FlowQpsRunner { + private static final String KEY = "abc"; + + private static AtomicInteger pass = new AtomicInteger(); + private static AtomicInteger block = new AtomicInteger(); + private static AtomicInteger total = new AtomicInteger(); + + private static volatile boolean stop = false; + + private static final int threadCount = 1; + private static int seconds = 60 + 40; + + public void simulateTraffic() { + for (int i = 0; i < threadCount; i++) { + Thread t = new Thread(new RunTask()); + t.setName("simulate-traffic-Task"); + t.start(); + } + } + + public void tick() { + Thread timer = new Thread(new TimerTask()); + timer.setName("sentinel-timer-task"); + timer.start(); + } + + static final class RunTask implements Runnable { + @Override + public void run() { + while (!stop) { + Entry entry = null; + + try { + entry = SphU.entry(KEY); + // token acquired, means pass + pass.addAndGet(1); + } catch (BlockException e1) { + block.incrementAndGet(); + } catch (Exception e2) { + // biz exception + } finally { + total.incrementAndGet(); + if (entry != null) { + entry.exit(); + } + } + + Random random2 = new Random(); + try { + TimeUnit.MILLISECONDS.sleep(random2.nextInt(50)); + } catch (InterruptedException e) { + // ignore + } + } + } + } + + static final class TimerTask implements Runnable { + @Override + public void run() { + long start = System.currentTimeMillis(); + System.out.println("begin to statistic!!!"); + + long oldTotal = 0; + long oldPass = 0; + long oldBlock = 0; + while (!stop) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + } + long globalTotal = total.get(); + long oneSecondTotal = globalTotal - oldTotal; + oldTotal = globalTotal; + + long globalPass = pass.get(); + long oneSecondPass = globalPass - oldPass; + oldPass = globalPass; + + long globalBlock = block.get(); + long oneSecondBlock = globalBlock - oldBlock; + oldBlock = globalBlock; + + System.out.println(seconds + " send qps is: " + oneSecondTotal); + System.out.println(TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + + ", pass:" + oneSecondPass + + ", block:" + oneSecondBlock); + + if (seconds-- <= 0) { + stop = true; + } + } + + long cost = System.currentTimeMillis() - start; + System.out.println("time cost: " + cost + " ms"); + System.out.println("total:" + total.get() + ", pass:" + pass.get() + + ", block:" + block.get()); + System.exit(0); + } + } +} diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonDegradeRuleListParser.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonDegradeRuleListParser.java new file mode 100644 index 00000000..bb0c4d50 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonDegradeRuleListParser.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.file.rule.parser; + +import java.util.List; + +import com.alibaba.csp.sentinel.datasource.ConfigParser; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +/** + * A {@link ConfigParser} parses Json String to {@code List}. + * + * @author Carpenter Lee + */ +public class JsonDegradeRuleListParser implements ConfigParser> { + @Override + public List parse(String source) { + return JSON.parseObject(source, new TypeReference>() {}); + } +} diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonFlowRuleListParser.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonFlowRuleListParser.java new file mode 100644 index 00000000..044d440d --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonFlowRuleListParser.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.file.rule.parser; + +import java.util.List; + +import com.alibaba.csp.sentinel.datasource.ConfigParser; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +/** + * A {@link ConfigParser} parses Json String to {@code List}. + * + * @author Carpenter Lee + */ +public class JsonFlowRuleListParser implements ConfigParser> { + @Override + public List parse(String source) { + return JSON.parseObject(source, new TypeReference>() {}); + } +} diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonSystemRuleListParser.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonSystemRuleListParser.java new file mode 100644 index 00000000..59ffeb0f --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/parser/JsonSystemRuleListParser.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.file.rule.parser; + +import java.util.List; + +import com.alibaba.csp.sentinel.datasource.ConfigParser; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +/** + * A {@link ConfigParser} parses Json String to {@code List}. + * + * @author Carpenter Lee + */ +public class JsonSystemRuleListParser implements ConfigParser> { + @Override + public List parse(String source) { + return JSON.parseObject(source, new TypeReference>() {}); + } +} diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/DegradeRule.json b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/DegradeRule.json new file mode 100644 index 00000000..5977c5fc --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/DegradeRule.json @@ -0,0 +1,16 @@ +[ + { + "resource": "abc0", + "count": 20.0, + "grade": 0, + "passCount": 0, + "timeWindow": 10 + }, + { + "resource": "abc1", + "count": 15.0, + "grade": 0, + "passCount": 0, + "timeWindow": 10 + } +] \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/FlowRule.json b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/FlowRule.json new file mode 100644 index 00000000..3c622f28 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/FlowRule.json @@ -0,0 +1,18 @@ +[ + { + "resource": "abc", + "controlBehavior": 0, + "count": 20.0, + "grade": 1, + "limitApp": "default", + "strategy": 0 + }, + { + "resource": "abc1", + "controlBehavior": 0, + "count": 20.0, + "grade": 1, + "limitApp": "default", + "strategy": 0 + } +] \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/SystemRule.json b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/SystemRule.json new file mode 100644 index 00000000..1069f130 --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/resources/SystemRule.json @@ -0,0 +1,8 @@ +[ + { + "avgRt": 10, + "highestSystemLoad": 5.0, + "maxThread": 10, + "qps": 20.0 + } +] \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-rocketmq/README.md b/sentinel-demo/sentinel-demo-rocketmq/README.md new file mode 100644 index 00000000..55d50c6e --- /dev/null +++ b/sentinel-demo/sentinel-demo-rocketmq/README.md @@ -0,0 +1,15 @@ +# Sentinel RocketMQ Demo + +This demonstrates some specific scenarios for Apache RocketMQ client. + +## Uniform Rate Limiting + +In Apache RocketMQ, when message consumers are consuming messages, there may a sudden inflow of messages, whether using pull or push mode. If all the messages were handled at this time, it would be likely to cause the system to be overloaded and then affect stability. However, in fact, there may be no messages coming within a few seconds. If redundant messages are directly discarded, the system's ability to process the message is not fully utilized. We hope that the sudden inflow of messages can be spread over a period of time, so that the system load can be kept on the stable level while processing as many messages as possible, thus achieving the effect of “shaving the peaks and filling the valley”. + +![shaving the peaks and filling the valley](https://github.com/alibaba/Sentinel/wiki/image/mq-traffic-peak-clipping-en.png) + +Sentinel provides a feature for this kind of scenario: [Rate Limiter](https://github.com/alibaba/Sentinel/wiki/Flow-Shaping:-Pace-Limiter), which can spread a large number of sudden request inflow in a uniform rate manner, let the request pass at a fixed interval. It is often used to process burst requests instead of rejecting them. This avoids traffic spurs causing system overloaded. Moreover, the pending requests will be queued and processed one by one. When the request is estimated to exceed the maximum queuing timeout, it will be rejected immediately. + +For example, we configure the rule with uniform rate limiting mode and QPS count is 5, which indicates messages are consumed at fixed interval (200 ms) and pending messages will queue. We also set the maximum queuing timeout is 5s, then all requests estimated to exceed the timeout will be rejected immediately. + +![Uniform rate](https://github.com/alibaba/Sentinel/wiki/image/uniform-speed-queue.png) \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-rocketmq/pom.xml b/sentinel-demo/sentinel-demo-rocketmq/pom.xml new file mode 100755 index 00000000..de5b4972 --- /dev/null +++ b/sentinel-demo/sentinel-demo-rocketmq/pom.xml @@ -0,0 +1,36 @@ + + + + com.alibaba.csp + sentinel-demo + 0.1.0 + + 4.0.0 + + sentinel-demo-rocketmq + + + + org.apache.rocketmq + rocketmq-client + 4.2.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + 1.8 + 1.8 + ${java.encoding} + + + + + \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/Constants.java b/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/Constants.java new file mode 100755 index 00000000..2f67dc6c --- /dev/null +++ b/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/Constants.java @@ -0,0 +1,24 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.rocketmq; + +public final class Constants { + + public static final String TEST_GROUP_NAME = "sentinel-group"; + public static final String TEST_TOPIC_NAME = "SentinelTopicTest"; + + private Constants() {} +} diff --git a/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/PullConsumerDemo.java b/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/PullConsumerDemo.java new file mode 100755 index 00000000..fd1d0d1d --- /dev/null +++ b/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/PullConsumerDemo.java @@ -0,0 +1,151 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.rocketmq; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +public class PullConsumerDemo { + + private static final String KEY = String.format("%s:%s", Constants.TEST_GROUP_NAME, Constants.TEST_TOPIC_NAME); + + private static final Map OFFSET_TABLE = new HashMap(); + + private static final ExecutorService pool = Executors.newFixedThreadPool(32); + + private static final AtomicLong SUCCESS_COUNT = new AtomicLong(0); + private static final AtomicLong FAIL_COUNT = new AtomicLong(0); + + public static void main(String[] args) throws MQClientException { + // First we init the flow control rule for Sentinel. + initFlowControlRule(); + + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(Constants.TEST_GROUP_NAME); + + consumer.start(); + + Set mqs = consumer.fetchSubscribeMessageQueues(Constants.TEST_TOPIC_NAME); + for (MessageQueue mq : mqs) { + System.out.printf("Consuming messages from the queue: %s%n", mq); + SINGLE_MQ: + while (true) { + try { + PullResult pullResult = + consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32); + if (pullResult.getMsgFoundList() != null) { + for (MessageExt msg : pullResult.getMsgFoundList()) { + doSomething(msg); + } + } + + long nextOffset = pullResult.getNextBeginOffset(); + putMessageQueueOffset(mq, nextOffset); + consumer.updateConsumeOffset(mq, nextOffset); + switch (pullResult.getPullStatus()) { + case FOUND: + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + break SINGLE_MQ; + case OFFSET_ILLEGAL: + break; + default: + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + consumer.shutdown(); + } + + private static void doSomething(MessageExt message) { + pool.submit(() -> { + Entry entry = null; + try { + ContextUtil.enter(KEY); + entry = SphU.entry(KEY, EntryType.OUT); + + // Your business logic here. + System.out.printf("[%d][%s][Success: %d] Receive New Messages: %s %n", System.currentTimeMillis(), + Thread.currentThread().getName(), SUCCESS_COUNT.addAndGet(1), new String(message.getBody())); + } catch (BlockException ex) { + // Blocked. + System.out.println("Blocked: " + FAIL_COUNT.addAndGet(1)); + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + }); + } + + private static void initFlowControlRule() { + FlowRule rule = new FlowRule(); + rule.setResource(KEY); + // Indicates the interval between two adjacent requests is 200 ms. + rule.setCount(5); + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setLimitApp("default"); + + // Enable rate limiting (uniform). This can ensure fixed intervals between two adjacent calls. + // In this example, intervals between two incoming calls (message consumption) will be 200 ms constantly. + rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); + // If more requests are coming, they'll be put into the waiting queue. + // The queue has a queueing timeout. Requests that may exceed the timeout will be immediately blocked. + // In this example, the max timeout is 5s. + rule.setMaxQueueingTimeMs(5 * 1000); + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + private static long getMessageQueueOffset(MessageQueue mq) { + Long offset = OFFSET_TABLE.get(mq); + if (offset != null) { + return offset; + } + + return 0; + } + + private static void putMessageQueueOffset(MessageQueue mq, long offset) { + OFFSET_TABLE.put(mq, offset); + } + +} \ No newline at end of file diff --git a/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/SyncProducer.java b/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/SyncProducer.java new file mode 100755 index 00000000..8cae72a1 --- /dev/null +++ b/sentinel-demo/sentinel-demo-rocketmq/src/main/java/com/alibaba/csp/sentinel/demo/rocketmq/SyncProducer.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.demo.rocketmq; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class SyncProducer { + + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name. + DefaultMQProducer producer = new + DefaultMQProducer(Constants.TEST_GROUP_NAME); + // Launch the instance. + producer.start(); + for (int i = 0; i < 1000; i++) { + // Create a message instance, specifying topic, tag and message body. + Message msg = new Message(Constants.TEST_TOPIC_NAME, "TagA", + ("Hello RocketMQ From Sentinel " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) + ); + // Call send message to deliver message to one of brokers. + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + // Shut down once the producer instance is not longer in use. + producer.shutdown(); + } +} \ No newline at end of file diff --git a/sentinel-extension/README.md b/sentinel-extension/README.md new file mode 100755 index 00000000..5522d9a4 --- /dev/null +++ b/sentinel-extension/README.md @@ -0,0 +1,9 @@ +# Sentinel Extension + +This is the parent of all extension points to Sentinel. + +Examples of what makes sense as a extension submodule are: + +* alternate implementations rules config + + diff --git a/sentinel-extension/pom.xml b/sentinel-extension/pom.xml new file mode 100755 index 00000000..363ec0e1 --- /dev/null +++ b/sentinel-extension/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-parent + 0.1.0 + + sentinel-extension + pom + + + sentinel-datasource-extension + + + diff --git a/sentinel-extension/sentinel-datasource-extension/pom.xml b/sentinel-extension/sentinel-datasource-extension/pom.xml new file mode 100755 index 00000000..d83cf0a7 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-extension + 0.1.0 + + + sentinel-datasource-extension + jar + + + + com.alibaba.csp + sentinel-core + + + com.alibaba + fastjson + + + junit + junit + test + + + diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AbstractDataSource.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AbstractDataSource.java new file mode 100755 index 00000000..f9e060b2 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AbstractDataSource.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.datasource; + +import com.alibaba.csp.sentinel.property.DynamicSentinelProperty; +import com.alibaba.csp.sentinel.property.SentinelProperty; + +public abstract class AbstractDataSource implements DataSource { + + protected ConfigParser parser; + protected SentinelProperty property; + + public AbstractDataSource(ConfigParser parser) { + if (parser == null) { + throw new IllegalArgumentException("parser can't be null"); + } + this.parser = parser; + this.property = new DynamicSentinelProperty(); + } + + @Override + public T loadConfig() throws Exception { + S readValue = readSource(); + T value = parser.parse(readValue); + return value; + } + + public T loadConfig(S conf) throws Exception { + T value = parser.parse(conf); + return value; + } + + @Override + public SentinelProperty getProperty() { + return property; + } + + @Override + public void writeDataSource(T values) throws Exception { + throw new UnsupportedOperationException(); + } + +} diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AutoRefreshDataSource.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AutoRefreshDataSource.java new file mode 100755 index 00000000..bff3d588 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AutoRefreshDataSource.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.datasource; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + * A {@link DataSource} automatically fetches the backend data. + * + * @param source data type + * @param target data type + * @author Carpenter Lee + */ +public abstract class AutoRefreshDataSource extends AbstractDataSource { + + private ScheduledExecutorService service; + protected long recommendRefreshMs = 3000; + + public AutoRefreshDataSource(ConfigParser configParser) { + super(configParser); + startTimerService(); + } + + public AutoRefreshDataSource(ConfigParser configParser, final long recommendRefreshMs) { + super(configParser); + if (recommendRefreshMs <= 0) { + throw new IllegalArgumentException("recommendRefreshMs must > 0, but " + recommendRefreshMs + " get"); + } + this.recommendRefreshMs = recommendRefreshMs; + startTimerService(); + } + + private void startTimerService() { + service = Executors.newScheduledThreadPool(1); + service.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + T newValue = loadConfig(); + + getProperty().updateValue(newValue); + } catch (Throwable e) { + RecordLog.info("loadConfig exception", e); + } + } + }, recommendRefreshMs, recommendRefreshMs, TimeUnit.MILLISECONDS); + } + + @Override + public void close() throws Exception { + if (service != null) { + service.shutdownNow(); + service = null; + } + } + +} diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/ConfigParser.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/ConfigParser.java new file mode 100755 index 00000000..3302a4cd --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/ConfigParser.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.datasource; + +/** + * Parse config from source data type S to target data type T. + * + * @author leyou + */ +public interface ConfigParser { + /** + * Parse {@code source} to the target format. + * + * @param source the source. + * @return the target. + */ + T parse(S source); +} diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/DataSource.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/DataSource.java new file mode 100755 index 00000000..6cdea924 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/DataSource.java @@ -0,0 +1,66 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.datasource; + +import com.alibaba.csp.sentinel.property.SentinelProperty; + +/** + * This class is responsible for getting configs. + * + * @param source data type + * @param target data type + * @author leyou + */ +public interface DataSource { + + /** + * Load data data source as the target type. + * + * @return the target data. + * @throws Exception + */ + T loadConfig() throws Exception; + + /** + * Read original data from the data source. + * + * @return the original data. + * @throws Exception + */ + S readSource() throws Exception; + + /** + * Get {@link SentinelProperty} of the data source. + * + * @return the property. + */ + SentinelProperty getProperty(); + + /** + * Write the {@code values} to the data source. + * + * @param values + * @throws Exception + */ + void writeDataSource(T values) throws Exception; + + /** + * Close the data source. + * + * @throws Exception + */ + void close() throws Exception; +} diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/EmptyDataSource.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/EmptyDataSource.java new file mode 100755 index 00000000..574e6692 --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/EmptyDataSource.java @@ -0,0 +1,60 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.datasource; + +import com.alibaba.csp.sentinel.property.NoOpSentinelProperty; +import com.alibaba.csp.sentinel.property.SentinelProperty; + +/** + * A {@link DataSource} based on nothing. {@link EmptyDataSource#getProperty()} will always return the same cached + * {@link SentinelProperty} that doing nothing. + *
    + * This class is used when we want to use default settings instead of configs from the {@link DataSource} + * + * @author leyou + */ +public class EmptyDataSource implements DataSource { + + public static final DataSource EMPTY_DATASOURCE = new EmptyDataSource(); + + private static final SentinelProperty property = new NoOpSentinelProperty(); + + private EmptyDataSource() { } + + @Override + public Object loadConfig() throws Exception { + return null; + } + + @Override + public Object readSource() throws Exception { + return null; + } + + @Override + public SentinelProperty getProperty() { + return property; + } + + @Override + public void close() throws Exception { } + + @Override + public void writeDataSource(Object config) throws Exception { + throw new UnsupportedOperationException(); + } + +} diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileRefreshableDataSource.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileRefreshableDataSource.java new file mode 100755 index 00000000..6d3ea7da --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileRefreshableDataSource.java @@ -0,0 +1,134 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.datasource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + *

    + * A {@link DataSource} based on file. This class will automatically fetches the backend file every 3 seconds. + *

    + *

    + * Limitations: default read buffer size is 1MB, if file size is greater than buffer size, exceeding bytes will + * be ignored. Default charset is UTF8. + *

    + * + * @author Carpenter Lee + */ +public class FileRefreshableDataSource extends AutoRefreshDataSource { + + private static final int MAX_SIZE = 1024 * 1024 * 4; + private static final long DEFAULT_REFRESH_MS = 3000; + private static final int DEFAULT_BUF_SIZE = 1024 * 1024; + private static final Charset DEFAULT_CHAR_SET = Charset.forName("utf-8"); + + private byte[] buf; + private Charset charset; + private File file; + + /** + * Create a file based {@link DataSource} whose read buffer size is 1MB, charset is UTF8, + * and read interval is 3 seconds. + * + * @param file the file to read. + * @param configParser the config parser. + */ + public FileRefreshableDataSource(File file, ConfigParser configParser) throws FileNotFoundException { + this(file, configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, DEFAULT_CHAR_SET); + } + + public FileRefreshableDataSource(String fileName, ConfigParser configParser) + throws FileNotFoundException { + this(new File(fileName), configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, DEFAULT_CHAR_SET); + //System.out.println(file.getAbsoluteFile()); + } + + public FileRefreshableDataSource(File file, ConfigParser configParser, int bufSize) + throws FileNotFoundException { + this(file, configParser, DEFAULT_REFRESH_MS, bufSize, DEFAULT_CHAR_SET); + } + + public FileRefreshableDataSource(File file, ConfigParser configParser, Charset charset) + throws FileNotFoundException { + this(file, configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, charset); + } + + public FileRefreshableDataSource(File file, ConfigParser configParser, long recommendRefreshMs, + int bufSize, Charset charset) throws FileNotFoundException { + super(configParser, recommendRefreshMs); + if (bufSize <= 0 || bufSize > MAX_SIZE) { + throw new IllegalArgumentException("bufSize must between (0, " + MAX_SIZE + "], but " + bufSize + " get"); + } + if (file == null) { + throw new IllegalArgumentException("file can't be null"); + } + if (charset == null) { + throw new IllegalArgumentException("charset can't be null"); + } + this.buf = new byte[bufSize]; + this.file = file; + this.charset = charset; + firstLoad(); + } + + private void firstLoad() { + try { + T newValue = loadConfig(); + getProperty().updateValue(newValue); + } catch (Throwable e) { + RecordLog.info("loadConfig exception", e); + } + } + + @Override + public String readSource() throws Exception { + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + FileChannel channel = inputStream.getChannel(); + if (channel.size() > buf.length) { + throw new RuntimeException(file.getAbsolutePath() + " file size=" + channel.size() + + ", is bigger than bufSize=" + buf.length + ". Can't read"); + } + int len = inputStream.read(buf); + return new String(buf, 0, len, charset); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + } + + @Override + public void close() throws Exception { + super.close(); + buf = null; + } + + @Override + public void writeDataSource(T values) throws Exception { + throw new UnsupportedOperationException(); + } +} diff --git a/sentinel-transport/pom.xml b/sentinel-transport/pom.xml new file mode 100755 index 00000000..26478332 --- /dev/null +++ b/sentinel-transport/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + pom + + com.alibaba.csp + sentinel-parent + 0.1.0 + + sentinel-transport + The transport module of Sentinel + + + sentinel-transport-common + + sentinel-transport-simple-http + sentinel-transport-netty-http + + + + + + com.alibaba.csp + sentinel-core + ${project.version} + + + com.alibaba.csp + sentinel-datasource-extension + ${project.version} + + + + + junit + junit + test + + + + diff --git a/sentinel-transport/sentinel-transport-common/pom.xml b/sentinel-transport/sentinel-transport-common/pom.xml new file mode 100755 index 00000000..4846db58 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/pom.xml @@ -0,0 +1,24 @@ + + + + com.alibaba.csp + sentinel-transport + 0.1.0 + + 4.0.0 + jar + sentinel-transport-common + + + + com.alibaba.csp + sentinel-datasource-extension + + + com.alibaba + fastjson + + + \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandler.java new file mode 100755 index 00000000..0adb598c --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandler.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command; + +/** + * Represent a handler that handles a {@link CommandRequest}. + * + * @author Eric Zhao + */ +public interface CommandHandler { + + /** + * Handle the given Courier command request. + * + * @param request the request to handle + * @return the response + */ + CommandResponse handle(CommandRequest request); +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java new file mode 100755 index 00000000..a39c24ba --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandHandlerProvider.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.ServiceLoader; + +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Provides and filters command handlers registered via SPI. + * + * @author Eric Zhao + */ +public class CommandHandlerProvider implements Iterable { + + private final ServiceLoader serviceLoader = ServiceLoader.load(CommandHandler.class); + + /** + * Get all command handlers annotated with {@link CommandMapping} with command name. + * + * @return list of all named command handlers + */ + public Map namedHandlers() { + Map map = new HashMap(); + for (CommandHandler handler : serviceLoader) { + String name = parseCommandName(handler); + if (!StringUtil.isEmpty(name)) { + map.put(name, handler); + } + } + return map; + } + + private String parseCommandName(CommandHandler handler) { + CommandMapping commandMapping = handler.getClass().getAnnotation(CommandMapping.class); + if (commandMapping != null) { + return commandMapping.name(); + } else { + return null; + } + } + + @Override + public Iterator iterator() { + return serviceLoader.iterator(); + } + + private static final CommandHandlerProvider INSTANCE = new CommandHandlerProvider(); + + public static CommandHandlerProvider getInstance() { + return INSTANCE; + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandRequest.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandRequest.java new file mode 100755 index 00000000..cb2ff12a --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Command request representation of command center. + * + * @author Eric Zhao + */ +public class CommandRequest { + + private final Map metadata = new HashMap(); + private final Map parameters = new HashMap(); + private byte[] body; + + public byte[] getBody() { + return body; + } + + public CommandRequest setBody(byte[] body) { + this.body = body; + return this; + } + + public Map getParameters() { + return parameters; + } + + public String getParam(String key) { + return parameters.get(key); + } + + public String getParam(String key, String defaultValue) { + String value = parameters.get(key); + return StringUtil.isBlank(value) ? defaultValue : value; + } + + public CommandRequest addParam(String key, String value) { + if (StringUtil.isBlank(key)) { + throw new IllegalArgumentException("Parameter key cannot be empty"); + } + parameters.put(key, value); + return this; + } + + public Map getMetadata() { + return metadata; + } + + public CommandRequest addMetadata(String key, String value) { + if (StringUtil.isBlank(key)) { + throw new IllegalArgumentException("Metadata key cannot be empty"); + } + metadata.put(key, value); + return this; + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandResponse.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandResponse.java new file mode 100755 index 00000000..2993d2d7 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/CommandResponse.java @@ -0,0 +1,83 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command; + +/** + * Command response representation of command center. + * + * @param type of the result + * @author Eric Zhao + */ +public class CommandResponse { + + private final boolean success; + private final R result; + private final Throwable exception; + + private CommandResponse(R result) { + this(result, true, null); + } + + private CommandResponse(R result, boolean success, Throwable exception) { + this.success = success; + this.result = result; + this.exception = exception; + } + + /** + * Construct a successful response with given object. + * + * @param result result object + * @param type of the result + * @return constructed server response + */ + public static CommandResponse ofSuccess(T result) { + return new CommandResponse(result); + } + + /** + * Construct a failed response with given exception. + * + * @param ex cause of the failure + * @return constructed server response + */ + public static CommandResponse ofFailure(Throwable ex) { + return new CommandResponse(null, false, ex); + } + + /** + * Construct a failed response with given exception. + * + * @param ex cause of the failure + * @param result additional message of the failure + * @return constructed server response + */ + public static CommandResponse ofFailure(Throwable ex, T result) { + return new CommandResponse(result, false, ex); + } + + public boolean isSuccess() { + return success; + } + + public R getResult() { + return result; + } + + public Throwable getException() { + return exception; + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/annotation/CommandMapping.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/annotation/CommandMapping.java new file mode 100755 index 00000000..ad7f353c --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/annotation/CommandMapping.java @@ -0,0 +1,33 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Eric Zhao + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Documented +public @interface CommandMapping { + + String name(); +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/BasicInfoCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/BasicInfoCommandHandler.java new file mode 100755 index 00000000..fbd5f892 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/BasicInfoCommandHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.util.HostNameUtil; + +/** + * The basic info command returns the runtime properties. + * + * @author Eric Zhao + */ +@CommandMapping(name = "basicInfo") +public class BasicInfoCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + return CommandResponse.ofSuccess(HostNameUtil.getConfigString()); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchActiveRuleCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchActiveRuleCommandHandler.java new file mode 100755 index 00000000..3e427d46 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchActiveRuleCommandHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; +import com.alibaba.fastjson.JSON; + +/** + * @author jialiang.linjl + */ +@CommandMapping(name = "getRules") +public class FetchActiveRuleCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + String type = request.getParam("type"); + if ("flow".equalsIgnoreCase(type)) { + return CommandResponse.ofSuccess(JSON.toJSONString(FlowRuleManager.getRules())); + } else if ("degrade".equalsIgnoreCase(type)) { + return CommandResponse.ofSuccess(JSON.toJSONString(DegradeRuleManager.getRules())); + } else if ("authority".equalsIgnoreCase(type)) { + return CommandResponse.ofSuccess(JSON.toJSONString(AuthorityRuleManager.getRules())); + } else if ("system".equalsIgnoreCase(type)) { + return CommandResponse.ofSuccess(JSON.toJSONString(SystemRuleManager.getRules())); + } else { + return CommandResponse.ofFailure(new IllegalArgumentException("invalid type")); + } + } + +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeByIdCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeByIdCommandHandler.java new file mode 100755 index 00000000..9b9a9cc1 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeByIdCommandHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.fastjson.JSON; + +/** + * @author qinan.qn + */ +@CommandMapping(name = "clusterNodeById") +public class FetchClusterNodeByIdCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + String id = request.getParam("id"); + if (StringUtil.isEmpty(id)) { + return CommandResponse.ofFailure(new IllegalArgumentException("Invalid parameter: empty clusterNode name")); + } + ClusterNode node = ClusterBuilderSlot.getClusterNode(id); + if (node != null) { + return CommandResponse.ofSuccess(JSON.toJSONString(NodeVo.fromClusterNode(id, node))); + } else { + return CommandResponse.ofSuccess("{}"); + } + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeHumanCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeHumanCommandHandler.java new file mode 100755 index 00000000..e3707e0b --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchClusterNodeHumanCommandHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.util.Map.Entry; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/** + * @author qinan.qn + */ +@CommandMapping(name = "cnode") +public class FetchClusterNodeHumanCommandHandler implements CommandHandler { + + private final static String FORMAT = "%-4s%-80s%-10s%-10s%-10s%-11s%-9s%-6s%-10s%-11s%-9s%-11s"; + private final static int MAX_LEN = 79; + + @Override + public CommandResponse handle(CommandRequest request) { + String name = request.getParam("id"); + + if (StringUtil.isEmpty(name)) { + return CommandResponse.ofFailure(new IllegalArgumentException("Invalid parameter: empty clusterNode name")); + } + + StringBuilder sb = new StringBuilder(); + + int i = 0; + int nameLength = 0; + for (Entry e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) { + if (e.getKey().getName().contains(name)) { + int l = e.getKey().getShowName().length(); + if (l > nameLength) { + nameLength = l; + } + if (++i == 30) { + break; + } + } + + } + nameLength = nameLength > MAX_LEN ? MAX_LEN : nameLength; + String format = FORMAT.replaceAll("80", String.valueOf(nameLength + 1)); + + sb.append(String.format(format, "idx", "id", "thread", "pass", "blocked", "success", "total", "aRt", + "1m-pass", "1m-block", "1m-all", "exception")).append("\n"); + for (Entry e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) { + if (e.getKey().getName().contains(name)) { + ClusterNode node = e.getValue(); + String id = e.getKey().getShowName(); + int lenNum = (int)Math.ceil((double)id.length() / nameLength) - 1; + + sb.append(String.format(format, i + 1, lenNum == 0 ? id : id.substring(0, nameLength), + node.curThreadNum(), node.passQps(), node.blockedQps(), node.successQps(), node.totalQps(), + node.avgRt(), node.totalRequest() - node.blockedRequest(), node.blockedRequest(), + node.totalRequest(), node.exceptionQps())).append("\n"); + for (int j = 1; j <= lenNum; ++j) { + int start = nameLength * j; + int end = j == lenNum ? id.length() : nameLength * (j + 1); + sb.append(String.format(format, "", id.substring(start, end), "", "", "", "", "", "", "", "", "", + "", "", "")).append("\n"); + } + + if (++i == 30) { + break; + } + } + } + + return CommandResponse.ofSuccess(sb.toString()); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchJsonTreeCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchJsonTreeCommandHandler.java new file mode 100755 index 00000000..e8f82397 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchJsonTreeCommandHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.command.vo.NodeVo; + +import com.alibaba.fastjson.JSON; + +/** + * @author leyou + */ +@CommandMapping(name = "jsonTree") +public class FetchJsonTreeCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + List results = new ArrayList(); + visit(Constants.ROOT, results, null); + return CommandResponse.ofSuccess(JSON.toJSONString(results)); + } + + /** + * Preorder traversal. + */ + private void visit(DefaultNode node, List results, String parentId) { + NodeVo vo = NodeVo.fromDefaultNode(node, parentId); + results.add(vo); + String id = vo.getId(); + for (Node n : node.getChildList()) { + visit((DefaultNode)n, results, id); + } + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchOriginCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchOriginCommandHandler.java new file mode 100755 index 00000000..0b7a4942 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchOriginCommandHandler.java @@ -0,0 +1,112 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.util.Map.Entry; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.StatisticNode; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; + +/** + * @author qinan.qn + */ +@CommandMapping(name = "origin") +public class FetchOriginCommandHandler implements CommandHandler { + + private final static String FORMAT = "%-4s%-80s%-10s%-10s%-11s%-9s%-6s%-10s%-11s%-9s"; + private final static int MAX_LEN = 79; + + @Override + public CommandResponse handle(CommandRequest request) { + StringBuilder sb = new StringBuilder(); + String name = request.getParam("id"); + + ClusterNode cNode = null; + + boolean exactly = false; + for (Entry e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) { + if (e.getKey().getName().equals(name)) { + cNode = e.getValue(); + sb.append("id: ").append(e.getKey().getShowName()).append("\n"); + sb.append("\n"); + exactly = true; + break; + } + } + + if (!exactly) { + for (Entry e : ClusterBuilderSlot.getClusterNodeMap().entrySet()) { + if (e.getKey().getName().indexOf(name) > 0) { + cNode = e.getValue(); + sb.append("id: ").append(e.getKey().getShowName()).append("\n"); + sb.append("\n"); + break; + } + } + } + + if (cNode == null) { + return CommandResponse.ofSuccess("Not find cNode with id " + name); + } + int i = 0; + int nameLength = 0; + for (Entry e : cNode.getOriginCountMap().entrySet()) { + int l = e.getKey().length(); + if (l > nameLength) { + nameLength = l; + } + if (++i == 120) { + break; + } + } + nameLength = nameLength > MAX_LEN ? MAX_LEN : nameLength; + String format = FORMAT.replaceAll("80", String.valueOf(nameLength + 1)); + i = 0; + sb.append(String + .format(format, "idx", "origin", "threadNum", "passedQps", "blockedQps", "totalQps", "aRt", "1m-passed", + "1m-blocked", "1m-total")).append("\n"); + + for (Entry e : cNode.getOriginCountMap().entrySet()) { + StatisticNode node = e.getValue(); + String id = e.getKey(); + int lenNum = (int)Math.ceil((double)id.length() / nameLength) - 1; + sb.append(String + .format(format, i + 1, lenNum == 0 ? id : id.substring(0, nameLength), node.curThreadNum(), + node.passQps(), node.blockedQps(), node.totalQps(), node.avgRt(), + node.totalRequest() - node.blockedRequest(), node.blockedRequest(), node.totalRequest())) + .append("\n"); + for (int j = 1; j <= lenNum; ++j) { + int start = nameLength * j; + int end = j == lenNum ? id.length() : nameLength * (j + 1); + sb.append(String + .format(format, "", id.substring(start, end), "", "", "", "", "", "", "", "", "", "", "", "")) + .append("\n"); + } + if (++i == 30) { + break; + } + + } + + return CommandResponse.ofSuccess(sb.toString()); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSimpleClusterNodeCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSimpleClusterNodeCommandHandler.java new file mode 100755 index 00000000..375fadde --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSimpleClusterNodeCommandHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.fastjson.JSONArray; + +/** + * @author jialiang.linjl + */ +@CommandMapping(name = "clusterNode") +public class FetchSimpleClusterNodeCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + /* + * type==notZero means nodes whose totalRequest <= 0 will be ignored. + */ + String type = request.getParam("type"); + List list = new ArrayList(); + Map map = ClusterBuilderSlot.getClusterNodeMap(); + if (map == null) { + return CommandResponse.ofSuccess(JSONArray.toJSONString(list)); + } + for (Map.Entry entry : map.entrySet()) { + if ("notZero".equalsIgnoreCase(type)) { + if (entry.getValue().totalRequest() > 0) { + list.add(NodeVo.fromClusterNode(entry.getKey(), entry.getValue())); + } + } else { + list.add(NodeVo.fromClusterNode(entry.getKey(), entry.getValue())); + } + } + return CommandResponse.ofSuccess(JSONArray.toJSONString(list)); + } + +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSystemStatusCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSystemStatusCommandHandler.java new file mode 100755 index 00000000..fd75ab88 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchSystemStatusCommandHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.fastjson.JSONObject; + +/** + * @author jialiang.linjl + */ +@CommandMapping(name = "systemStatus") +public class FetchSystemStatusCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + + Map systemStatus = new HashMap(); + + systemStatus.put("rqps", Constants.ENTRY_NODE.successQps()); + systemStatus.put("qps", Constants.ENTRY_NODE.passQps()); + systemStatus.put("b", Constants.ENTRY_NODE.blockedQps()); + systemStatus.put("r", Constants.ENTRY_NODE.avgRt()); + systemStatus.put("t", Constants.ENTRY_NODE.curThreadNum()); + + return CommandResponse.ofSuccess(JSONObject.toJSONString(systemStatus)); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTreeCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTreeCommandHandler.java new file mode 100755 index 00000000..c2093262 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/FetchTreeCommandHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; + +/** + * @author qinan.qn + */ +@CommandMapping(name = "tree") +public class FetchTreeCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + String id = request.getParam("id"); + + StringBuilder sb = new StringBuilder(); + + DefaultNode start = Constants.ROOT; + + if (id == null) { + visitTree(0, start, sb); + } else { + boolean exactly = false; + for (Node n : start.getChildList()) { + DefaultNode dn = (DefaultNode)n; + if (dn.getId().getName().equals(id)) { + visitTree(0, dn, sb); + exactly = true; + break; + } + } + + if (!exactly) { + for (Node n : start.getChildList()) { + DefaultNode dn = (DefaultNode)n; + if (dn.getId().getName().contains(id)) { + visitTree(0, dn, sb); + } + } + } + } + sb.append("\r\n\r\n"); + sb.append( + "t:threadNum pq:passQps bq:blockedQps tq:totalQps rt:averageRt prq: passRequestQps 1mp:1m-passed " + + "1mb:1m-blocked 1mt:1m-total").append("\r\n"); + return CommandResponse.ofSuccess(sb.toString()); + } + + private void visitTree(int level, DefaultNode node, /*@NonNull*/ StringBuilder sb) { + for (int i = 0; i < level; ++i) { + sb.append("-"); + } + if (!(node instanceof EntranceNode)) { + sb.append(String.format("%s(t:%s pq:%s bq:%s tq:%s rt:%s prq:%s 1mp:%s 1mb:%s 1mt:%s)", + node.getId().getShowName(), node.curThreadNum(), node.passQps(), + node.blockedQps(), node.totalQps(), node.avgRt(), node.successQps(), + node.totalRequest() - node.blockedRequest(), node.blockedRequest(), + node.totalRequest())).append("\n"); + } else { + sb.append(String.format("EntranceNode: %s(t:%s pq:%s bq:%s tq:%s rt:%s prq:%s 1mp:%s 1mb:%s 1mt:%s)", + node.getId().getShowName(), node.curThreadNum(), node.passQps(), + node.blockedQps(), node.totalQps(), node.avgRt(), node.successQps(), + node.totalRequest() - node.blockedRequest(), node.blockedRequest(), + node.totalRequest())).append("\n"); + } + for (Node n : node.getChildList()) { + DefaultNode dn = (DefaultNode)n; + visitTree(level + 1, dn, sb); + } + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java new file mode 100755 index 00000000..de78311b --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.net.URLDecoder; +import java.util.List; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.datasource.DataSource; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRule; +import com.alibaba.fastjson.JSONArray; + +/** + * @author jialiang.linjl + */ +@CommandMapping(name = "setRules") +public class ModifyRulesCommandHandler implements CommandHandler { + + static DataSource> flowDataSource = null; + static DataSource> authorityDataSource = null; + static DataSource> degradeDataSource = null; + static DataSource> systemSource = null; + + public static synchronized void registerFlowDataSource(DataSource> datasource) { + flowDataSource = datasource; + } + + public static synchronized void registerAuthorityDataSource(DataSource> dataSource) { + authorityDataSource = dataSource; + } + + public static synchronized void registerDegradeDataSource(DataSource> dataSource) { + degradeDataSource = dataSource; + } + + public static synchronized void registerSystemDataSource(DataSource> dataSource) { + systemSource = dataSource; + } + + @Override + public CommandResponse handle(CommandRequest request) { + String type = request.getParam("type"); + // rule data in get parameter + String data = request.getParam("data"); + if (StringUtil.isNotEmpty(data)) { + try { + data = URLDecoder.decode(data, "utf-8"); + } catch (Exception e) { + RecordLog.info("decode rule data error", e); + return CommandResponse.ofFailure(e, "decode rule data error"); + } + } + + RecordLog.info("receive rule change:" + type); + RecordLog.info(data); + + String result = "success"; + + if ("flow".equalsIgnoreCase(type)) { + List flowRules = JSONArray.parseArray(data, FlowRule.class); + FlowRuleManager.loadRules(flowRules); + if (flowDataSource != null) { + try { + flowDataSource.writeDataSource(flowRules); + } catch (Exception e) { + result = "partial success"; + RecordLog.info(e.getMessage(), e); + } + } + return CommandResponse.ofSuccess(result); + } else if ("authority".equalsIgnoreCase(type)) { + List rules = JSONArray.parseArray(data, AuthorityRule.class); + AuthorityRuleManager.loadRules(rules); + if (authorityDataSource != null) { + try { + authorityDataSource.writeDataSource(rules); + } catch (Exception e) { + result = "partial success"; + RecordLog.info(e.getMessage(), e); + } + } + return CommandResponse.ofSuccess(result); + } else if ("degrade".equalsIgnoreCase(type)) { + List rules = JSONArray.parseArray(data, DegradeRule.class); + DegradeRuleManager.loadRules(rules); + if (degradeDataSource != null) { + try { + degradeDataSource.writeDataSource(rules); + } catch (Exception e) { + result = "partial success"; + RecordLog.info(e.getMessage(), e); + } + } + return CommandResponse.ofSuccess(result); + } else if ("system".equalsIgnoreCase(type)) { + List rules = JSONArray.parseArray(data, SystemRule.class); + SystemRuleManager.loadRules(rules); + if (systemSource != null) { + try { + systemSource.writeDataSource(rules); + } catch (Exception e) { + result = "partial success"; + RecordLog.info(e.getMessage(), e); + } + } + return CommandResponse.ofSuccess(result); + } + return CommandResponse.ofFailure(new IllegalArgumentException("invalid type")); + + } + +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffGetCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffGetCommandHandler.java new file mode 100755 index 00000000..476b4efe --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffGetCommandHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; + +/** + * @author youji.zj + */ +@CommandMapping(name = "getSwitch") +public class OnOffGetCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + return CommandResponse.ofSuccess("Sentinel switch value: " + Constants.ON); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffSetCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffSetCommandHandler.java new file mode 100755 index 00000000..7854a4db --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/OnOffSetCommandHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.Constants; + +/** + * @author youji.zj + */ +@CommandMapping(name = "setSwitch") +public class OnOffSetCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + String value = request.getParam("value"); + + try { + Constants.ON = Boolean.valueOf(value); + } catch (Exception e) { + RecordLog.info("Bad value when setting global switch", e); + } + + String info = "Sentinel set switch value: " + value; + RecordLog.info(info); + + return CommandResponse.ofSuccess(info); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/SendMetricCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/SendMetricCommandHandler.java new file mode 100755 index 00000000..1fe605d4 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/SendMetricCommandHandler.java @@ -0,0 +1,96 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import java.util.List; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; +import com.alibaba.csp.sentinel.util.PidUtil; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.csp.sentinel.node.metric.MetricNode; +import com.alibaba.csp.sentinel.node.metric.MetricSearcher; +import com.alibaba.csp.sentinel.node.metric.MetricWriter; + +/** + * Retrieve and aggregate {@link MetricNode} metrics. + * + * @author leyou + * @author Eric Zhao + */ +@CommandMapping(name = "metric") +public class SendMetricCommandHandler implements CommandHandler { + + private MetricSearcher searcher; + + private final Object lock = new Object(); + + @Override + public CommandResponse handle(CommandRequest request) { + // Note: not thread-safe. + if (searcher == null) { + synchronized (lock) { + String appName = SentinelConfig.getAppName(); + if (appName == null) { + appName = ""; + } + if (searcher == null) { + searcher = new MetricSearcher(MetricWriter.METRIC_BASE_DIR, + MetricWriter.formMetricFileName(appName, PidUtil.getPid())); + } + } + } + String startTimeStr = request.getParam("startTime"); + String endTimeStr = request.getParam("endTime"); + String maxLinesStr = request.getParam("maxLines"); + String identity = request.getParam("identity"); + long startTime = -1; + int maxLines = 6000; + if (StringUtil.isNotBlank(startTimeStr)) { + startTime = Long.parseLong(startTimeStr); + } else { + return CommandResponse.ofSuccess(""); + } + List list; + try { + // Find by end time if set. + if (StringUtil.isNotBlank(endTimeStr)) { + long endTime = Long.parseLong(endTimeStr); + list = searcher.findByTimeAndResource(startTime, endTime, identity); + } else { + if (StringUtil.isNotBlank(maxLinesStr)) { + maxLines = Integer.parseInt(maxLinesStr); + } + maxLines = Math.min(maxLines, 12000); + list = searcher.find(startTime, maxLines); + } + } catch (Exception ex) { + return CommandResponse.ofFailure(new RuntimeException("Error when retrieving metrics", ex)); + } + + if (list == null) { + return CommandResponse.ofSuccess("No metrics"); + } + StringBuilder sb = new StringBuilder(); + for (MetricNode node : list) { + sb.append(node.toThinString()).append("\n"); + } + return CommandResponse.ofSuccess(sb.toString()); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/VersionCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/VersionCommandHandler.java new file mode 100755 index 00000000..1a99f0d3 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/VersionCommandHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.handler; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.command.annotation.CommandMapping; + +/** + * @author jialiang.linjl + * @author Eric Zhao + */ +@CommandMapping(name = "version") +public class VersionCommandHandler implements CommandHandler { + + @Override + public CommandResponse handle(CommandRequest request) { + return CommandResponse.ofSuccess("0.1.0"); + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/vo/NodeVo.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/vo/NodeVo.java new file mode 100755 index 00000000..417b0cfe --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/vo/NodeVo.java @@ -0,0 +1,238 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.command.vo; + +import java.util.UUID; + +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; + +/** + * This class is view object of {@link DefaultNode} or {@link ClusterNode}. + * + * @author leyou + */ +public class NodeVo { + + private String id; + private String parentId; + private String resource; + + private Integer threadNum; + private Long passQps; + private Long blockedQps; + private Long totalQps; + private Long averageRt; + private Long successQps; + private Long exceptionQps; + private Long oneMinutePassed; + private Long oneMinuteBlocked; + private Long oneMinuteException; + private Long oneMinuteTotal; + + private Long timestamp; + + /** + * {@link DefaultNode} holds statistics of every node in the invoke tree. + * We use parentId to hold the tree structure. + * + * @param node the DefaultNode to be presented. + * @param parentId random generated parent node id, may be a random UUID + * @return node view object. + */ + public static NodeVo fromDefaultNode(DefaultNode node, String parentId) { + if (node == null) { + return null; + } + NodeVo vo = new NodeVo(); + vo.id = UUID.randomUUID().toString(); + vo.parentId = parentId; + vo.resource = node.getId().getShowName(); + vo.threadNum = node.curThreadNum(); + vo.passQps = node.passQps(); + vo.blockedQps = node.blockedQps(); + vo.totalQps = node.totalQps(); + vo.averageRt = node.avgRt(); + vo.successQps = node.successQps(); + vo.exceptionQps = node.exceptionQps(); + vo.oneMinuteException = node.totalException(); + vo.oneMinutePassed = node.totalRequest() - node.blockedRequest(); + vo.oneMinuteBlocked = node.blockedRequest(); + vo.oneMinuteTotal = node.totalRequest(); + vo.timestamp = System.currentTimeMillis(); + return vo; + } + + /** + * {@link ClusterNode} holds total statistics of the same resource name. + * + * @param name resource name. + * @param node the ClusterNode to be presented. + * @return node view object. + */ + public static NodeVo fromClusterNode(ResourceWrapper name, ClusterNode node) { + return fromClusterNode(name.getShowName(), node); + } + + /** + * {@link ClusterNode} holds total statistics of the same resource name. + * + * @param name resource name. + * @param node the ClusterNode to be presented. + * @return node view object. + */ + public static NodeVo fromClusterNode(String name, ClusterNode node) { + if (node == null) { + return null; + } + NodeVo vo = new NodeVo(); + vo.resource = name; + vo.threadNum = node.curThreadNum(); + vo.passQps = node.passQps(); + vo.blockedQps = node.blockedQps(); + vo.totalQps = node.totalQps(); + vo.averageRt = node.avgRt(); + vo.successQps = node.successQps(); + vo.exceptionQps = node.exceptionQps(); + vo.oneMinuteException = node.totalException(); + vo.oneMinutePassed = node.totalRequest() - node.blockedRequest(); + vo.oneMinuteBlocked = node.blockedRequest(); + vo.oneMinuteTotal = node.totalRequest(); + vo.timestamp = System.currentTimeMillis(); + return vo; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getThreadNum() { + return threadNum; + } + + public void setThreadNum(Integer threadNum) { + this.threadNum = threadNum; + } + + public Long getPassQps() { + return passQps; + } + + public void setPassQps(Long passQps) { + this.passQps = passQps; + } + + public Long getBlockedQps() { + return blockedQps; + } + + public void setBlockedQps(Long blockedQps) { + this.blockedQps = blockedQps; + } + + public Long getTotalQps() { + return totalQps; + } + + public void setTotalQps(Long totalQps) { + this.totalQps = totalQps; + } + + public Long getAverageRt() { + return averageRt; + } + + public void setAverageRt(Long averageRt) { + this.averageRt = averageRt; + } + + public Long getSuccessQps() { + return successQps; + } + + public void setSuccessQps(Long successQps) { + this.successQps = successQps; + } + + public Long getExceptionQps() { + return exceptionQps; + } + + public void setExceptionQps(Long exceptionQps) { + this.exceptionQps = exceptionQps; + } + + public Long getOneMinuteException() { + return oneMinuteException; + } + + public void setOneMinuteException(Long oneMinuteException) { + this.oneMinuteException = oneMinuteException; + } + + public Long getOneMinutePassed() { + return oneMinutePassed; + } + + public void setOneMinutePassed(Long oneMinutePassed) { + this.oneMinutePassed = oneMinutePassed; + } + + public Long getOneMinuteBlocked() { + return oneMinuteBlocked; + } + + public void setOneMinuteBlocked(Long oneMinuteBlocked) { + this.oneMinuteBlocked = oneMinuteBlocked; + } + + public Long getOneMinuteTotal() { + return oneMinuteTotal; + } + + public void setOneMinuteTotal(Long oneMinuteTotal) { + this.oneMinuteTotal = oneMinuteTotal; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/CommandCenter.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/CommandCenter.java new file mode 100755 index 00000000..f78833ec --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/CommandCenter.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport; + +/** + * @author Eric Zhao + */ +public interface CommandCenter { + + /** + * Prepare and init for the command center (e.g. register commands). + * This will be executed before starting. + * + * @throws Exception if error occurs + */ + void beforeStart() throws Exception; + + /** + * Start the command center in the background. + * This method should NOT block. + * + * @throws Exception if error occurs + */ + void start() throws Exception; + + /** + * Stop the command center and do cleanup. + * + * @throws Exception if error occurs + */ + void stop() throws Exception; +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/HeartbeatSender.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/HeartbeatSender.java new file mode 100755 index 00000000..b7eec31f --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/HeartbeatSender.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport; + +/** + * Heartbeat interface. Sentinel core is responsible for invoking {@link #sendHeartbeat()} + * at every {@link #intervalMs()} interval. + * + * @author leyou + * @author Eric Zhao + */ +public interface HeartbeatSender { + + /** + * Send heartbeat to Sentinel Dashboard. Each invocation of this method will send + * heartbeat once. Sentinel core is responsible for invoking this method + * at every {@link #intervalMs()} interval. + * + * @return whether heartbeat is successfully send. + * @throws Exception + */ + boolean sendHeartbeat() throws Exception; + + /** + * Millisecond interval of every {@link #sendHeartbeat()} + * + * @return millisecond interval. + */ + long intervalMs(); +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/client/CommandClient.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/client/CommandClient.java new file mode 100755 index 00000000..13b94556 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/client/CommandClient.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.client; + +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; + +/** + * Basic interface for clients that sending commands. + * + * @author Eric Zhao + */ +public interface CommandClient { + + /** + * Send a command to target destination. + * + * @param host target host + * @param port target port + * @param request command request + * @return the response from target command server + * @throws Exception when unexpected error occurs + */ + CommandResponse sendCommand(String host, int port, CommandRequest request) throws Exception; +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java new file mode 100755 index 00000000..791db903 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/config/TransportConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.config; + +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * @author leyou + */ +public class TransportConfig { + + public static final String CONSOLE_SERVER = "csp.sentinel.dashboard.server"; + public static final String SERVER_PORT = "csp.sentinel.api.port"; + public static final String HEARTBEAT_INTERVAL_MS = "csp.sentinel.heartbeat.interval.ms"; + + private static int runtimePort = -1; + + public static Long getHeartbeatIntervalMs() { + String interval = SentinelConfig.getConfig(HEARTBEAT_INTERVAL_MS); + return interval == null ? null : Long.parseLong(interval); + } + + /** + * Get ip:port of Sentinel Dashboard. + * + * @return console server ip:port, maybe null if not configured + */ + public static String getConsoleServer() { + return SentinelConfig.getConfig(CONSOLE_SERVER); + } + + /** + * Get Server port of this HTTP server. + * + * @return the port, maybe null if not configured. + */ + public static String getPort() { + if (runtimePort > 0) { + return String.valueOf(runtimePort); + } + return SentinelConfig.getConfig(SERVER_PORT); + } + + /** + * Set real port this HTTP server uses. + * + * @param port real port. + */ + public static void setRuntimePort(int port) { + runtimePort = port; + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/CommandCenterInitFunc.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/CommandCenterInitFunc.java new file mode 100755 index 00000000..6b974907 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/CommandCenterInitFunc.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.init; + +import java.util.Iterator; +import java.util.ServiceLoader; + +import com.alibaba.csp.sentinel.init.InitFunc; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.transport.CommandCenter; + +/** + * @author Eric Zhao + */ +public class CommandCenterInitFunc implements InitFunc { + + @Override + public void init() throws Exception { + ServiceLoader loader = ServiceLoader.load(CommandCenter.class); + Iterator iterator = loader.iterator(); + if (iterator.hasNext()) { + CommandCenter commandCenter = iterator.next(); + if (iterator.hasNext()) { + throw new IllegalStateException("Only single command center can be started"); + } else { + commandCenter.beforeStart(); + commandCenter.start(); + RecordLog.info("[CommandCenterInit] Starting command center: " + + commandCenter.getClass().getCanonicalName()); + } + } + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/HeartbeatSenderInitFunc.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/HeartbeatSenderInitFunc.java new file mode 100755 index 00000000..cf42f6a0 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/init/HeartbeatSenderInitFunc.java @@ -0,0 +1,74 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.init; + +import java.util.Iterator; +import java.util.ServiceLoader; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.alibaba.csp.sentinel.init.InitFunc; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.transport.HeartbeatSender; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; + +/** + * Global init function for heartbeat sender. + * + * @author Eric Zhao + */ +public class HeartbeatSenderInitFunc implements InitFunc { + + private static ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); + + @Override + public void init() throws Exception { + long heartBeatInterval = -1; + try { + heartBeatInterval = TransportConfig.getHeartbeatIntervalMs(); + RecordLog.info("system property heartbeat interval set: " + heartBeatInterval); + } catch (Exception ex) { + RecordLog.info("Parse heartbeat interval failed, use that in code, " + ex.getMessage()); + } + ServiceLoader loader = ServiceLoader.load(HeartbeatSender.class); + Iterator iterator = loader.iterator(); + if (iterator.hasNext()) { + final HeartbeatSender sender = iterator.next(); + if (iterator.hasNext()) { + throw new IllegalStateException("Only single heartbeat sender can be scheduled"); + } else { + long interval = sender.intervalMs(); + if (heartBeatInterval != -1) { + interval = heartBeatInterval; + } + pool.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + sender.sendHeartbeat(); + } catch (Exception e) { + e.printStackTrace(); + RecordLog.info("[HeartbeatSender] Send heartbeat error", e); + } + } + }, 10000, interval, TimeUnit.MILLISECONDS); + RecordLog.info("[HeartbeatSenderInit] HeartbeatSender started: " + + sender.getClass().getCanonicalName()); + } + } + } +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/util/HttpCommandUtils.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/util/HttpCommandUtils.java new file mode 100755 index 00000000..17321e61 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/transport/util/HttpCommandUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.util; + +import com.alibaba.csp.sentinel.command.CommandRequest; + +/** + * Util class for HTTP command center. + * + * @author Eric Zhao + */ +public final class HttpCommandUtils { + + public static final String REQUEST_TARGET = "command-target"; + + public static String getTarget(CommandRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + return request.getMetadata().get(REQUEST_TARGET); + } + + private HttpCommandUtils() {} +} diff --git a/sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler b/sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler new file mode 100755 index 00000000..1dd95c88 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.command.CommandHandler @@ -0,0 +1,14 @@ +com.alibaba.csp.sentinel.command.handler.BasicInfoCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchActiveRuleCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchClusterNodeByIdCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchClusterNodeHumanCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchJsonTreeCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchOriginCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchSimpleClusterNodeCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchSystemStatusCommandHandler +com.alibaba.csp.sentinel.command.handler.FetchTreeCommandHandler +com.alibaba.csp.sentinel.command.handler.ModifyRulesCommandHandler +com.alibaba.csp.sentinel.command.handler.OnOffGetCommandHandler +com.alibaba.csp.sentinel.command.handler.OnOffSetCommandHandler +com.alibaba.csp.sentinel.command.handler.SendMetricCommandHandler +com.alibaba.csp.sentinel.command.handler.VersionCommandHandler \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc b/sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc new file mode 100755 index 00000000..c3ce2960 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc @@ -0,0 +1,2 @@ +com.alibaba.csp.sentinel.transport.init.CommandCenterInitFunc +com.alibaba.csp.sentinel.transport.init.HeartbeatSenderInitFunc \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-netty-http/pom.xml b/sentinel-transport/sentinel-transport-netty-http/pom.xml new file mode 100755 index 00000000..c12088d6 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/pom.xml @@ -0,0 +1,41 @@ + + + + com.alibaba.csp + sentinel-transport + 0.1.0 + + 4.0.0 + sentinel-transport-netty-http + jar + + + + com.alibaba.csp + sentinel-transport-common + ${project.version} + + + io.netty + netty-all + 4.1.26.Final + + + com.alibaba + fastjson + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + org.apache.httpcomponents + httpcore + 4.4.5 + + + \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/NettyHttpCommandCenter.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/NettyHttpCommandCenter.java new file mode 100755 index 00000000..3b118c65 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/NettyHttpCommandCenter.java @@ -0,0 +1,67 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command; + +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandHandlerProvider; +import com.alibaba.csp.sentinel.transport.command.netty.HttpServer; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.transport.CommandCenter; + +/** + * Implementation of {@link CommandCenter} based on Netty HTTP library. + * + * @author Eric Zhao + */ +public class NettyHttpCommandCenter implements CommandCenter { + + private final HttpServer server = new HttpServer(); + + private final ExecutorService pool = Executors.newSingleThreadExecutor(); + + @Override + public void start() throws Exception { + pool.submit(new Runnable() { + @Override + public void run() { + try { + server.start(); + } catch (Exception ex) { + RecordLog.info("Start netty server error", ex); + ex.printStackTrace(); + System.exit(-1); + } + } + }); + } + + @Override + public void stop() throws Exception { + server.close(); + pool.shutdownNow(); + } + + @Override + public void beforeStart() throws Exception { + // Register handlers + Map handlers = CommandHandlerProvider.getInstance().namedHandlers(); + server.registerCommands(handlers); + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/CodecRegistry.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/CodecRegistry.java new file mode 100755 index 00000000..1261ce2e --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/CodecRegistry.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.codec; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Eric Zhao + */ +public final class CodecRegistry { + + private final List> encoderList = new ArrayList>(); + private final List> decoderList = new ArrayList>(); + + public CodecRegistry() { + // Register default codecs. + registerEncoder(DefaultCodecs.STRING_ENCODER); + + registerDecoder(DefaultCodecs.STRING_DECODER); + } + + public void registerEncoder(Encoder encoder) { + encoderList.add(encoder); + } + + public void registerDecoder(Decoder decoder) { + decoderList.add(decoder); + } + + public List> getEncoderList() { + return encoderList; + } + + public List> getDecoderList() { + return decoderList; + } + + public void reset() { + encoderList.clear(); + decoderList.clear(); + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Decoder.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Decoder.java new file mode 100755 index 00000000..3a0ff7c8 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Decoder.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.codec; + +import java.nio.charset.Charset; + +/** + * The decoder decodes bytes into an object of type {@code }. + * + * @param target type + * @author Eric Zhao + */ +public interface Decoder { + + /** + * Check whether the decoder supports the given target type. + * + * @param clazz type of the class + * @return {@code true} if supported, {@code false} otherwise + */ + boolean canDecode(Class clazz); + + /** + * Decode the given byte array into an object of type {@code R} with the default charset. + * + * @param bytes raw byte buffer + * @return the decoded target object + * @throws Exception error occurs when decoding the object (e.g. IO fails) + */ + R decode(byte[] bytes) throws Exception; + + /** + * Decode the given byte array into an object of type {@code R} with the given charset. + * + * @param bytes raw byte buffer + * @param charset the charset + * @return the decoded target object + * @throws Exception error occurs when decoding the object (e.g. IO fails) + */ + R decode(byte[] bytes, Charset charset) throws Exception; +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/DefaultCodecs.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/DefaultCodecs.java new file mode 100755 index 00000000..595eb540 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/DefaultCodecs.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.codec; + +/** + * Caches default encoders and decoders. + * + * @author Eric Zhao + */ +final class DefaultCodecs { + + public static final Encoder STRING_ENCODER = new StringEncoder(); + + public static final Decoder STRING_DECODER = new StringDecoder(); + + private DefaultCodecs() {} +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Encoder.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Encoder.java new file mode 100755 index 00000000..634c24cf --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/Encoder.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.codec; + +import java.nio.charset.Charset; + +/** + * The encoder encodes an object of type {@code } into byte array. + * + * @param source type + * @author Eric Zhao + */ +public interface Encoder { + + /** + * Check whether the encoder supports the given source type. + * + * @param clazz type of the class + * @return {@code true} if supported, {@code false} otherwise + */ + boolean canEncode(Class clazz); + + /** + * Encode the given object into a byte array with the given charset. + * + * @param r the object to encode + * @param charset the charset + * @return the encoded byte buffer + * @throws Exception error occurs when encoding the object (e.g. IO fails) + */ + byte[] encode(R r, Charset charset) throws Exception; + + /** + * Encode the given object into a byte array with the default charset. + * + * @param r the object to encode + * @return the encoded byte buffer, witch is already flipped. + * @throws Exception error occurs when encoding the object (e.g. IO fails) + */ + byte[] encode(R r) throws Exception; +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringDecoder.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringDecoder.java new file mode 100755 index 00000000..89429e28 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringDecoder.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.codec; + +import java.nio.charset.Charset; + +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * Decodes from a byte array to string. + * + * @author Eric Zhao + */ +public class StringDecoder implements Decoder { + + @Override + public boolean canDecode(Class clazz) { + return String.class.isAssignableFrom(clazz); + } + + @Override + public String decode(byte[] bytes) throws Exception { + return decode(bytes, Charset.forName(SentinelConfig.charset())); + } + + @Override + public String decode(byte[] bytes, Charset charset) { + if (bytes == null || bytes.length <= 0) { + throw new IllegalArgumentException("Bad byte array"); + } + return new String(bytes, charset); + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringEncoder.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringEncoder.java new file mode 100755 index 00000000..50f07058 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/codec/StringEncoder.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.codec; + +import java.nio.charset.Charset; + +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * Encode a string to a byte array. + * + * @author Eric Zhao + */ +public class StringEncoder implements Encoder { + + @Override + public boolean canEncode(Class clazz) { + return String.class.isAssignableFrom(clazz); + } + + + + @Override + public byte[] encode(String string, Charset charset) { + return string.getBytes(charset); + } + + @Override + public byte[] encode(String s) { + return encode(s, Charset.forName(SentinelConfig.charset())); + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServer.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServer.java new file mode 100755 index 00000000..3ad0f65b --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServer.java @@ -0,0 +1,95 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.netty; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.log.CommandCenterLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +/** + * @author Eric Zhao + */ +public final class HttpServer { + + private static final int DEFAULT_PORT = 8719; + + private Channel channel; + + final static Map handlerMap = new ConcurrentHashMap(); + + public void start() throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new HttpServerInitializer()); + int port; + try { + if (StringUtil.isEmpty(TransportConfig.getPort())) { + CommandCenterLog.info("Port not configured, using default port: " + DEFAULT_PORT); + port = DEFAULT_PORT; + } else { + port = Integer.parseInt(TransportConfig.getPort()); + } + } catch (Exception e) { + throw new IllegalArgumentException("Illegal port: " + TransportConfig.getPort()); + } + channel = b.bind(port).sync().channel(); + channel.closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } + + public void close() { + channel.close(); + } + + public void registerCommand(String commandName, CommandHandler handler) { + if (StringUtil.isEmpty(commandName) || handler == null) { + return; + } + + if (handlerMap.containsKey(commandName)) { + CommandCenterLog.info("Register failed (duplicate command): " + commandName); + return; + } + + handlerMap.put(commandName, handler); + } + + public void registerCommands(Map handlerMap) { + if (handlerMap != null) { + for (Entry e : handlerMap.entrySet()) { + registerCommand(e.getKey(), e.getValue()); + } + } + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java new file mode 100755 index 00000000..e88343b0 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerHandler.java @@ -0,0 +1,210 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.netty; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.CommandCenterLog; +import com.alibaba.csp.sentinel.transport.command.codec.CodecRegistry; +import com.alibaba.csp.sentinel.transport.command.codec.Encoder; +import com.alibaba.csp.sentinel.transport.util.HttpCommandUtils; +import com.alibaba.csp.sentinel.util.StringUtil; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.QueryStringDecoder; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +/** + * Netty-based HTTP server handler for command center. + * + * Note: HTTP chunked is not tested! + * + * @author Eric Zhao + */ +public class HttpServerHandler extends SimpleChannelInboundHandler { + + private final CodecRegistry codecRegistry = new CodecRegistry(); + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + FullHttpRequest httpRequest = (FullHttpRequest)msg; + try { + CommandRequest request = parseRequest(httpRequest); + if (StringUtil.isBlank(HttpCommandUtils.getTarget(request))) { + writeErrorResponse(BAD_REQUEST.code(), "Invalid command", ctx); + return; + } + handleRequest(request, ctx, HttpUtil.isKeepAlive(httpRequest)); + + } catch (Exception ex) { + writeErrorResponse(INTERNAL_SERVER_ERROR.code(), SERVER_ERROR_MESSAGE, ctx); + CommandCenterLog.warn("Internal error", ex); + } + } + + private void handleRequest(CommandRequest request, ChannelHandlerContext ctx, boolean keepAlive) + throws Exception { + String commandName = HttpCommandUtils.getTarget(request); + // Find the matching command handler. + CommandHandler commandHandler = getHandler(commandName); + if (commandHandler != null) { + CommandResponse response = commandHandler.handle(request); + writeResponse(response, ctx, keepAlive); + } else { + // No matching command handler. + writeErrorResponse(BAD_REQUEST.code(), String.format("Unknown command \"%s\"", commandName), ctx); + } + } + + private Encoder pickEncoder(Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Bad class metadata"); + } + for (Encoder encoder : codecRegistry.getEncoderList()) { + if (encoder.canEncode(clazz)) { + return encoder; + } + } + return null; + } + + private void writeErrorResponse(int statusCode, String message, ChannelHandlerContext ctx) { + FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.valueOf(statusCode), + Unpooled.copiedBuffer(message, Charset.forName(SentinelConfig.charset()))); + + httpResponse.headers().set("Content-Type", "text/plain; charset=" + SentinelConfig.charset()); + ctx.write(httpResponse); + + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + + private void writeResponse(CommandResponse response, ChannelHandlerContext ctx, boolean keepAlive) + throws Exception { + byte[] body; + if (response.isSuccess()) { + if (response.getResult() == null) { + body = new byte[] {}; + } else { + Encoder encoder = pickEncoder(response.getResult().getClass()); + if (encoder == null) { + writeErrorResponse(INTERNAL_SERVER_ERROR.code(), SERVER_ERROR_MESSAGE, ctx); + CommandCenterLog.warn("Error when encoding object", + new IllegalStateException("No compatible encoder")); + return; + } + body = encoder.encode(response.getResult()); + } + } else { + body = response.getException().getMessage().getBytes(SentinelConfig.charset()); + } + + HttpResponseStatus status = response.isSuccess() ? OK : BAD_REQUEST; + + FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, + Unpooled.copiedBuffer(body)); + + httpResponse.headers().set("Content-Type", "text/plain; charset=" + SentinelConfig.charset()); + + //if (keepAlive) { + // httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); + // httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + //} + //ctx.write(httpResponse); + //if (!keepAlive) { + // ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + //} + httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); + httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + ctx.write(httpResponse); + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + + private CommandRequest parseRequest(FullHttpRequest request) { + QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); + CommandRequest serverRequest = new CommandRequest(); + Map> paramMap = queryStringDecoder.parameters(); + // Parse request parameters. + if (!paramMap.isEmpty()) { + for (Entry> p : paramMap.entrySet()) { + if (!p.getValue().isEmpty()) { + serverRequest.addParam(p.getKey(), p.getValue().get(0)); + } + } + } + // Parse command name. + String target = parseTarget(queryStringDecoder.rawPath()); + serverRequest.addMetadata(HttpCommandUtils.REQUEST_TARGET, target); + // Parse body. + if (request.content().readableBytes() <= 0) { + serverRequest.setBody(null); + } else { + serverRequest.setBody(request.content().array()); + } + return serverRequest; + } + + private String parseTarget(String uri) { + if (StringUtil.isEmpty(uri)) { + return ""; + } + String[] arr = uri.split("/"); + if (arr.length < 2) { + return ""; + } + return arr[1]; + } + + private CommandHandler getHandler(String commandName) { + if (StringUtil.isEmpty(commandName)) { + return null; + } + return HttpServer.handlerMap.get(commandName); + } + + private void send100Continue(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); + ctx.write(response); + } + + private static final String SERVER_ERROR_MESSAGE = "Command server error"; +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializer.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializer.java new file mode 100755 index 00000000..1138f788 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/command/netty/HttpServerInitializer.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.netty; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; + +/** + * @author Eric Zhao + */ +public class HttpServerInitializer extends ChannelInitializer { + + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline p = socketChannel.pipeline(); + + p.addLast(new HttpRequestDecoder()); + p.addLast(new HttpObjectAggregator(1024 * 1024)); + p.addLast(new HttpResponseEncoder()); + + p.addLast(new HttpServerHandler()); + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java new file mode 100755 index 00000000..976f26bb --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HttpHeartbeatSender.java @@ -0,0 +1,102 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat; + +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.AppNameUtil; +import com.alibaba.csp.sentinel.util.HostNameUtil; +import com.alibaba.csp.sentinel.transport.HeartbeatSender; +import com.alibaba.csp.sentinel.util.PidUtil; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +/** + * @author Eric Zhao + * @author leyou + */ +public class HttpHeartbeatSender implements HeartbeatSender { + + private final CloseableHttpClient client; + + private final int timeoutMs = 3000; + private final RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(timeoutMs) + .setConnectTimeout(timeoutMs) + .setSocketTimeout(timeoutMs) + .build(); + + private String consoleHost; + private int consolePort; + + public HttpHeartbeatSender() { + this.client = HttpClients.createDefault(); + String consoleServer = TransportConfig.getConsoleServer(); + if (StringUtil.isEmpty(consoleServer)) { + RecordLog.info("[Heartbeat] Console server address is not configured!"); + } else { + String consoleHost = consoleServer; + int consolePort = 80; + if (consoleServer.contains(",")) { + consoleHost = consoleServer.split(",")[0]; + } + if (consoleHost.contains(":")) { + String[] strs = consoleServer.split(":"); + consoleHost = strs[0]; + consolePort = Integer.parseInt(strs[1]); + } + this.consoleHost = consoleHost; + this.consolePort = consolePort; + } + + } + + @Override + public boolean sendHeartbeat() throws Exception { + if (StringUtil.isEmpty(consoleHost)) { + return false; + } + RecordLog.info(String.format("[Heartbeat] Sending heartbeat to %s:%d", consoleHost, consolePort)); + + URIBuilder uriBuilder = new URIBuilder(); + uriBuilder.setScheme("http").setHost(consoleHost).setPort(consolePort) + .setPath("/registry/machine") + .setParameter("app", AppNameUtil.getAppName()) + .setParameter("version", String.valueOf(System.currentTimeMillis())) + .setParameter("hostname", HostNameUtil.getHostName()) + .setParameter("ip", HostNameUtil.getIp()) + .setParameter("port", TransportConfig.getPort()) + .setParameter("pid", String.valueOf(PidUtil.getPid())); + + HttpGet request = new HttpGet(uriBuilder.build()); + request.setConfig(requestConfig); + // Send heartbeat request. + CloseableHttpResponse response = client.execute(request); + response.close(); + return true; + } + + @Override + public long intervalMs() { + return 5000; + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter b/sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter new file mode 100755 index 00000000..0512836e --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.transport.command.NettyHttpCommandCenter \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender b/sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender new file mode 100755 index 00000000..b0c16fcb --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.transport.heartbeat.HttpHeartbeatSender \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/pom.xml b/sentinel-transport/sentinel-transport-simple-http/pom.xml new file mode 100755 index 00000000..c2f8f343 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/pom.xml @@ -0,0 +1,21 @@ + + + + sentinel-transport + com.alibaba.csp + 0.1.0 + + 4.0.0 + + sentinel-transport-simple-http + + + + com.alibaba.csp + sentinel-transport-common + ${project.version} + + + \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/SimpleHttpCommandCenter.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/SimpleHttpCommandCenter.java new file mode 100755 index 00000000..66c4bb85 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/SimpleHttpCommandCenter.java @@ -0,0 +1,242 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandHandlerProvider; +import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory; +import com.alibaba.csp.sentinel.log.CommandCenterLog; +import com.alibaba.csp.sentinel.transport.CommandCenter; +import com.alibaba.csp.sentinel.transport.command.http.HttpEventTask; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/*** + * The simple command center provides service to exchange information. + * + * @author youji.zj + */ +public class SimpleHttpCommandCenter implements CommandCenter { + + private static final int PORT_UNINITIALIZED = -1; + + private static final int DEFAULT_SERVER_SO_TIMEOUT = 3000; + private static final int DEFAULT_PORT = 8719; + + private static final Map handlerMap = new HashMap(); + + private ExecutorService executor = Executors.newSingleThreadExecutor( + new NamedThreadFactory("sentinel-command-center-executor")); + private ExecutorService bizExecutor; + + private ServerSocket socketReference; + + @Override + public void beforeStart() throws Exception { + // Register handlers + Map handlers = CommandHandlerProvider.getInstance().namedHandlers(); + registerCommands(handlers); + } + + @Override + public void start() throws Exception { + int nThreads = Runtime.getRuntime().availableProcessors(); + this.bizExecutor = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue(10), + new NamedThreadFactory("sentinel-command-center-service-executor"), + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + CommandCenterLog.info("EventTask rejected"); + throw new RejectedExecutionException(); + } + }); + + Runnable serverInitTask = new Runnable() { + int port; + + { + try { + port = Integer.parseInt(TransportConfig.getPort()); + } catch (Exception e) { + port = DEFAULT_PORT; + } + } + + @Override + public void run() { + int repeat = 0; + + int tmpPort = port; + boolean success = false; + while (true) { + ServerSocket serverSocket = null; + try { + serverSocket = new ServerSocket(tmpPort); + } catch (IOException ex) { + CommandCenterLog.info( + String.format("IO error occurs, port: %d, repeat times: %d", tmpPort, repeat), ex); + + tmpPort = adjustPort(repeat); + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e1) { + break; + } + repeat++; + } + + if (serverSocket != null) { + CommandCenterLog.info("[CommandCenter] Begin listening at port " + serverSocket.getLocalPort()); + socketReference = serverSocket; + executor.submit(new ServerThread(serverSocket)); + success = true; + break; + } + } + + if (success) { + tmpPort = port; + } else { + tmpPort = PORT_UNINITIALIZED; + } + TransportConfig.setRuntimePort(tmpPort); + executor.shutdown(); + } + + /** + * Adjust the port to settle down. + */ + private int adjustPort(int repeat) { + int mod = repeat / 5; + return port + mod; + } + }; + + new Thread(serverInitTask).start(); + } + + @Override + public void stop() throws Exception { + if (socketReference != null) { + try { + socketReference.close(); + } catch (IOException e) { + CommandCenterLog.warn("Error when releasing the server socket", e); + } + } + bizExecutor.shutdownNow(); + executor.shutdownNow(); + TransportConfig.setRuntimePort(PORT_UNINITIALIZED); + handlerMap.clear(); + } + + /** + * Get the name set of all registered commands. + */ + public static Set getCommands() { + return handlerMap.keySet(); + } + + class ServerThread extends Thread { + + private ServerSocket serverSocket; + + ServerThread(ServerSocket s) { + this.serverSocket = s; + setName("sentinel-courier-server-accept-thread"); + } + + @Override + public void run() { + while (true) { + Socket socket = null; + try { + socket = this.serverSocket.accept(); + setSocketSoTimeout(socket); + HttpEventTask eventTask = new HttpEventTask(socket); + bizExecutor.submit(eventTask); + } catch (Exception e) { + CommandCenterLog.info("Server error", e); + if (socket != null) { + try { + socket.close(); + } catch (Exception e1) { + CommandCenterLog.info("Error when closing an opened socket", e1); + } + } + try { + // In case of infinite log. + Thread.sleep(10); + } catch (InterruptedException e1) { + // Indicates the task should stop. + break; + } + } + } + } + } + + public static CommandHandler getHandler(String commandName) { + return handlerMap.get(commandName); + } + + public static void registerCommands(Map handlerMap) { + if (handlerMap != null) { + for (Entry e : handlerMap.entrySet()) { + registerCommand(e.getKey(), e.getValue()); + } + } + } + + public static void registerCommand(String commandName, CommandHandler handler) { + if (StringUtil.isEmpty(commandName)) { + return; + } + + if (handlerMap.containsKey(commandName)) { + CommandCenterLog.info("Register failed (duplicate command): " + commandName); + return; + } + + handlerMap.put(commandName, handler); + } + + /** + * Avoid server thread hang, 3 seconds timeout by default. + */ + private void setSocketSoTimeout(Socket socket) throws SocketException { + if (socket != null) { + socket.setSoTimeout(DEFAULT_SERVER_SO_TIMEOUT); + } + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java new file mode 100755 index 00000000..35e3c92f --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java @@ -0,0 +1,229 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.command.http; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.net.URLDecoder; +import java.nio.charset.Charset; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.CommandCenterLog; +import com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter; +import com.alibaba.csp.sentinel.transport.util.HttpCommandUtils; +import com.alibaba.csp.sentinel.util.StringUtil; + +/*** + * The task handles incoming command request in HTTP protocol. + * + * @author youji.zj + * @author Eric Zhao + */ +public class HttpEventTask implements Runnable { + + private final Socket socket; + + private boolean writtenHead = false; + + public HttpEventTask(Socket socket) { + this.socket = socket; + } + + public void close() throws Exception { + socket.close(); + } + + @Override + public void run() { + if (socket == null) { + return; + } + + BufferedReader in = null; + PrintWriter printWriter = null; + try { + long start = System.currentTimeMillis(); + in = new BufferedReader(new InputStreamReader(socket.getInputStream(), SentinelConfig.charset())); + OutputStream outputStream = socket.getOutputStream(); + + printWriter = new PrintWriter( + new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset()))); + + String line = in.readLine(); + CommandCenterLog.info("[CommandCenter] socket income:" + line + + "," + socket.getInetAddress()); + CommandRequest request = parseRequest(line); + + // Validate the target command. + String commandName = HttpCommandUtils.getTarget(request); + if (StringUtil.isBlank(commandName)) { + badRequest(printWriter, "Invalid command"); + return; + } + + // Find the matching command handler. + CommandHandler commandHandler = SimpleHttpCommandCenter.getHandler(commandName); + if (commandHandler != null) { + CommandResponse response = commandHandler.handle(request); + handleResponse(response, printWriter, outputStream); + } else { + // No matching command handler. + badRequest(printWriter, "Unknown command `" + commandName + '`'); + } + printWriter.flush(); + + long cost = System.currentTimeMillis() - start; + CommandCenterLog.info("[CommandCenter] deal a socket task:" + line + + "," + socket.getInetAddress() + ", time cost=" + cost + " ms"); + } catch (Throwable e) { + CommandCenterLog.info("CommandCenter error", e); + try { + if (printWriter != null) { + String errorMessage = SERVER_ERROR_MESSAGE; + if (!writtenHead) { + internalError(printWriter, errorMessage); + } else { + printWriter.println(errorMessage); + } + printWriter.flush(); + } + } catch (Exception e1) { + CommandCenterLog.info("CommandCenter close serverSocket failed", e); + } + } finally { + closeResource(in); + closeResource(printWriter); + closeResource(socket); + } + } + + private void closeResource(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (Exception e) { + CommandCenterLog.info("CommandCenter close resource failed", e); + } + } + } + + private void handleResponse(CommandResponse response, /*@NonNull*/ final PrintWriter printWriter, + /*@NonNull*/ final OutputStream rawOutputStream) throws Exception { + if (response.isSuccess()) { + if (response.getResult() == null) { + writeOkStatusLine(printWriter); + return; + } + // Write 200 OK status line. + writeOkStatusLine(printWriter); + // Here we directly use `toString` to encode the result to plain text. + byte[] buffer = response.getResult().toString().getBytes(SentinelConfig.charset()); + rawOutputStream.write(buffer); + rawOutputStream.flush(); + } else { + String msg = SERVER_ERROR_MESSAGE; + if (response.getException() != null) { + msg = response.getException().getMessage(); + } + badRequest(printWriter, msg); + } + } + + /** + * Write `400 Bad Request` HTTP response status line and message body, then flush. + */ + private void badRequest(/*@NonNull*/ final PrintWriter out, String message) { + out.print("HTTP/1.1 400 Bad Request\r\n" + + "Connection: close\r\n\r\n"); + out.print(message); + out.flush(); + writtenHead = true; + } + + /** + * Write `500 Internal Server Error` HTTP response status line and message body, then flush. + */ + private void internalError(/*@NonNull*/ final PrintWriter out, String message) { + out.print("HTTP/1.1 500 Internal Server Error\r\n" + + "Connection: close\r\n\r\n"); + out.print(message); + out.flush(); + writtenHead = true; + } + + /** + * Write `200 OK` HTTP response status line and flush. + */ + private void writeOkStatusLine(/*@NonNull*/ final PrintWriter out) { + out.print("HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n\r\n"); + out.flush(); + writtenHead = true; + } + + /** + * Parse raw HTTP request line to a {@link CommandRequest}. + * + * @param line HTTP request line + * @return parsed command request + */ + private CommandRequest parseRequest(String line) { + CommandRequest request = new CommandRequest(); + if (StringUtil.isBlank(line)) { + return request; + } + int start = line.indexOf('/'); + int ask = line.indexOf('?') == -1 ? line.lastIndexOf(' ') : line.indexOf('?'); + int space = line.lastIndexOf(' '); + String target = line.substring(start != -1 ? start + 1 : 0, ask != -1 ? ask : line.length()); + request.addMetadata(HttpCommandUtils.REQUEST_TARGET, target); + if (ask == -1 || ask == space) { + return request; + } + String parameterStr = line.substring(ask != -1 ? ask + 1 : 0, space != -1 ? space : line.length()); + for (String parameter : parameterStr.split("&")) { + if (StringUtil.isBlank(parameter)) { + continue; + } + + String[] keyValue = parameter.split("="); + if (keyValue.length != 2) { + continue; + } + + String value = StringUtil.trim(keyValue[1]); + try { + value = URLDecoder.decode(value, SentinelConfig.charset()); + } catch (UnsupportedEncodingException e) { + } + + request.addParam(StringUtil.trim(keyValue[0]), value); + } + return request; + } + + private static final String SERVER_ERROR_MESSAGE = "Command server error"; + +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HeartbeatMessage.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HeartbeatMessage.java new file mode 100755 index 00000000..d6fc597f --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/HeartbeatMessage.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.util.AppNameUtil; +import com.alibaba.csp.sentinel.util.HostNameUtil; +import com.alibaba.csp.sentinel.util.TimeUtil; + +/** + * Heart beat message entity. + * The message consists of key-value pair parameters. + * + * @author leyou + */ +public class HeartbeatMessage { + + private final Map message = new HashMap(); + + public HeartbeatMessage() { + message.put("hostname", HostNameUtil.getHostName()); + message.put("ip", HostNameUtil.getIp()); + message.put("app", AppNameUtil.getAppName()); + message.put("port", String.valueOf(TransportConfig.getPort())); + } + + public HeartbeatMessage registerInformation(String key, String value) { + message.put(key, value); + return this; + } + + public Map generateCurrentMessage() { + message.put("version", String.valueOf(TimeUtil.currentTimeMillis())); + message.put("port", String.valueOf(TransportConfig.getPort())); + return message; + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java new file mode 100755 index 00000000..4eff03c9 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/SimpleHttpHeartbeatSender.java @@ -0,0 +1,118 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.transport.HeartbeatSender; +import com.alibaba.csp.sentinel.transport.config.TransportConfig; +import com.alibaba.csp.sentinel.transport.heartbeat.client.SimpleHttpClient; +import com.alibaba.csp.sentinel.transport.heartbeat.client.SimpleHttpRequest; +import com.alibaba.csp.sentinel.transport.heartbeat.client.SimpleHttpResponse; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * The heartbeat sender provides basic API for sending heartbeat request to provided target. + * This implementation is based on a trivial HTTP client. + * + * @author Eric Zhao + * @author leyou + */ +public class SimpleHttpHeartbeatSender implements HeartbeatSender { + + private static final String HEARTBEAT_PATH = "/registry/machine"; + private static final int OK_STATUS = 200; + + private static final long DEFAULT_INTERVAL = 1000 * 10; + + private final HeartbeatMessage heartBeat = new HeartbeatMessage(); + private final SimpleHttpClient httpClient = new SimpleHttpClient(); + + private final List addressList; + + private int currentAddressIdx = 0; + + public SimpleHttpHeartbeatSender() { + // Retrieve the list of default addresses. + List newAddrs = getDefaultConsoleIps(); + RecordLog.info("Default console address list retrieved: " + newAddrs); + this.addressList = newAddrs; + // Set interval config. + String interval = System.getProperty(TransportConfig.HEARTBEAT_INTERVAL_MS, String.valueOf(DEFAULT_INTERVAL)); + SentinelConfig.setConfig(TransportConfig.HEARTBEAT_INTERVAL_MS, interval); + } + + @Override + public boolean sendHeartbeat() throws Exception { + InetSocketAddress addr = getAvailableAddress(); + if (addr == null) { + return false; + } + + SimpleHttpRequest request = new SimpleHttpRequest(addr, HEARTBEAT_PATH); + request.setParams(heartBeat.generateCurrentMessage()); + try { + SimpleHttpResponse response = httpClient.post(request); + if (response.getStatusCode() == OK_STATUS) { + return true; + } + } catch (Exception e) { + RecordLog.info("Failed to send heart beat to " + addr + " : ", e); + } + return false; + } + + @Override + public long intervalMs() { + return DEFAULT_INTERVAL; + } + + private InetSocketAddress getAvailableAddress() { + if (addressList == null || addressList.isEmpty()) { + return null; + } + if (currentAddressIdx < 0) { + currentAddressIdx = 0; + } + int index = currentAddressIdx % addressList.size(); + return addressList.get(index); + } + + private List getDefaultConsoleIps() { + List newAddrs = new ArrayList(); + String ipsStr = TransportConfig.getConsoleServer(); + if (StringUtil.isEmpty(ipsStr)) { + return newAddrs; + } + + for (String ipPortStr : ipsStr.split(",")) { + if (ipPortStr.trim().length() == 0) { + continue; + } + String[] ipPort = ipPortStr.trim().split(":"); + int port = 80; + if (ipPort.length > 1) { + port = Integer.parseInt(ipPort[1].trim()); + } + newAddrs.add(new InetSocketAddress(ipPort[0].trim(), port)); + } + return newAddrs; + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpClient.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpClient.java new file mode 100755 index 00000000..8c20ca75 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpClient.java @@ -0,0 +1,190 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat.client; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.Map.Entry; + +import com.alibaba.csp.sentinel.log.CommandCenterLog; +import com.alibaba.csp.sentinel.log.RecordLog; + +/** + *

    + * A very simple HTTP client that only supports GET/POST method and plain text request body. + * The Content-Type header is always set as

    application/x-www-form-urlencoded
    . + * All parameters in the request will be encoded using {@link URLEncoder#encode(String, String)}. + *

    + *

    + * The result of a HTTP invocation will be wrapped as a {@link SimpleHttpResponse}. Content in response body + * will be automatically decoded to string with provided charset. + *

    + *

    + * This is a blocking and synchronous client, so an invocation will await the response until timeout exceed. + *

    + *

    + * Note that this is a very NAIVE client, {@code Content-Length} must be specified in the + * HTTP response header, otherwise, the response body will be dropped. All other body type such as + * {@code Transfer-Encoding: chunked}, {@code Transfer-Encoding: deflate} are not supported. + *

    + * + * @author leyou + */ +public class SimpleHttpClient { + + /** + * Execute a GET HTTP request. + * + * @param request HTTP request + * @return the response if the request is successful + * @throws IOException when connection cannot be established or the connection is interrupted + */ + public SimpleHttpResponse get(SimpleHttpRequest request) throws IOException { + if (request == null) { + return null; + } + return request(request.getSocketAddress(), + RequestMethod.GET, request.getRequestPath(), request.getParams(), + request.getCharset(), request.getSoTimeout()); + } + + /** + * Execute a POST HTTP request. + * + * @param request HTTP request + * @return the response if the request is successful + * @throws IOException when connection cannot be established or the connection is interrupted + */ + public SimpleHttpResponse post(SimpleHttpRequest request) throws IOException { + if (request == null) { + return null; + } + return request(request.getSocketAddress(), + RequestMethod.POST, request.getRequestPath(), + request.getParams(), request.getCharset(), + request.getSoTimeout()); + } + + private SimpleHttpResponse request(InetSocketAddress socketAddress, + RequestMethod type, String requestPath, + Map paramsMap, Charset charset, int soTimeout) + throws IOException { + Socket socket = null; + BufferedWriter writer; + try { + socket = new Socket(); + socket.setSoTimeout(soTimeout); + socket.connect(socketAddress, soTimeout); + + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), charset)); + requestPath = getRequestPath(type, requestPath, paramsMap, charset); + writer.write(getStatusLine(type, requestPath) + "\r\n"); + if (charset != null) { + writer.write("Content-Type: application/x-www-form-urlencoded; charset=" + charset.name() + "\r\n"); + } else { + writer.write("Content-Type: application/x-www-form-urlencoded\r\n"); + } + writer.write("Host: " + socketAddress.getHostName() + "\r\n"); + if (type == RequestMethod.GET) { + writer.write("Content-Length: 0\r\n"); + writer.write("\r\n"); + } else { + // POST method. + String params = encodeRequestParams(paramsMap, charset); + writer.write("Content-Length: " + params.getBytes(charset).length + "\r\n"); + writer.write("\r\n"); + writer.write(params); + } + writer.flush(); + + SimpleHttpResponse response = new SimpleHttpResponseParser().parse(socket.getInputStream()); + socket.close(); + socket = null; + return response; + } finally { + if (socket != null) { + try { + socket.close(); + } catch (Exception ex) { + CommandCenterLog.info("Error when closing " + type + " request to " + socketAddress + ": ", ex); + } + } + } + } + + private String getRequestPath(RequestMethod type, String requestPath, + Map paramsMap, Charset charset) { + if (type == RequestMethod.GET) { + if (requestPath.contains("?")) { + return requestPath + "&" + encodeRequestParams(paramsMap, charset); + } + return requestPath + "?" + encodeRequestParams(paramsMap, charset); + } + return requestPath; + } + + private String getStatusLine(RequestMethod type, String requestPath) { + if (type == RequestMethod.POST) { + return "POST " + requestPath + " HTTP/1.1"; + } + return "GET " + requestPath + " HTTP/1.1"; + } + + /** + * Encode and get the URL request parameters. + * + * @param paramsMap pair of parameters + * @param charset charset + * @return encoded request parameters, or empty string ("") if no parameters are provided + */ + private String encodeRequestParams(Map paramsMap, Charset charset) { + if (paramsMap == null || paramsMap.isEmpty()) { + return ""; + } + try { + StringBuilder paramsBuilder = new StringBuilder(); + for (Entry entry : paramsMap.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + paramsBuilder.append(URLEncoder.encode(entry.getKey(), charset.name())) + .append("=") + .append(URLEncoder.encode(entry.getValue(), charset.name())) + .append("&"); + } + if (paramsBuilder.length() > 0) { + // Remove the last '&'. + paramsBuilder.delete(paramsBuilder.length() - 1, paramsBuilder.length()); + } + return paramsBuilder.toString(); + } catch (Throwable e) { + RecordLog.info("Encode request params fail", e); + return ""; + } + } + + private enum RequestMethod { + GET, + POST + } + +} \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpRequest.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpRequest.java new file mode 100755 index 00000000..a9d3abdf --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpRequest.java @@ -0,0 +1,99 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat.client; + +import java.net.InetSocketAddress; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Simple HTTP request representation. + * + * @author leyou + */ +public class SimpleHttpRequest { + + private InetSocketAddress socketAddress; + private String requestPath = ""; + private int soTimeout = 3000; + private Map params; + private Charset charset = Charset.forName(SentinelConfig.charset()); + + public SimpleHttpRequest(InetSocketAddress socketAddress, String requestPath) { + this.socketAddress = socketAddress; + this.requestPath = requestPath; + } + + public InetSocketAddress getSocketAddress() { + return socketAddress; + } + + public SimpleHttpRequest setSocketAddress(InetSocketAddress socketAddress) { + this.socketAddress = socketAddress; + return this; + } + + public String getRequestPath() { + return requestPath; + } + + public SimpleHttpRequest setRequestPath(String requestPath) { + this.requestPath = requestPath; + return this; + } + + public int getSoTimeout() { + return soTimeout; + } + + public SimpleHttpRequest setSoTimeout(int soTimeout) { + this.soTimeout = soTimeout; + return this; + } + + public Map getParams() { + return params; + } + + public SimpleHttpRequest setParams(Map params) { + this.params = params; + return this; + } + + public Charset getCharset() { + return charset; + } + + public SimpleHttpRequest setCharset(Charset charset) { + this.charset = charset; + return this; + } + + public SimpleHttpRequest addParam(String key, String value) { + if (StringUtil.isBlank(key)) { + throw new IllegalArgumentException("Parameter key cannot be empty"); + } + if (params == null) { + params = new HashMap(); + } + params.put(key, value); + return this; + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponse.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponse.java new file mode 100755 index 00000000..196dbbb7 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponse.java @@ -0,0 +1,124 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat.client; + +import java.nio.charset.Charset; +import java.util.Map; + +import com.alibaba.csp.sentinel.config.SentinelConfig; + +/** + * Simple HTTP response representation. + * + * @author leyou + */ +public class SimpleHttpResponse { + + private Charset charset = Charset.forName(SentinelConfig.charset()); + + private String statusLine; + private int statusCode; + private Map headers; + private byte[] body; + + public SimpleHttpResponse(String statusLine, Map headers) { + this.statusLine = statusLine; + this.headers = headers; + } + + public SimpleHttpResponse(String statusLine, Map headers, byte[] body) { + this.statusLine = statusLine; + this.headers = headers; + this.body = body; + } + + private void parseCharset() { + String contentType = getHeader("Content-Type"); + for (String str : contentType.split(" ")) { + if (str.toLowerCase().startsWith("charset=")) { + charset = Charset.forName(str.split("=")[1]); + } + } + } + + private void parseCode() { + this.statusCode = Integer.parseInt(statusLine.split(" ")[1]); + } + + public void setBody(byte[] body) { + this.body = body; + } + + public byte[] getBody() { + return body; + } + + public String getStatusLine() { + return statusLine; + } + + public Integer getStatusCode() { + if (statusCode == 0) { + parseCode(); + } + return statusCode; + } + + public Map getHeaders() { + return headers; + } + + /** + * Get header of the key ignoring case. + * + * @param key header key + * @return header value + */ + public String getHeader(String key) { + if (headers == null) { + return null; + } + String value = headers.get(key); + if (value != null) { + return value; + } + for (Map.Entry entry : headers.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + return null; + } + + public String getBodyAsString() { + parseCharset(); + return new String(body, charset); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(statusLine) + .append("\r\n"); + for (Map.Entry entry : headers.entrySet()) { + buf.append(entry.getKey()).append(": ").append(entry.getValue()) + .append("\r\n"); + } + buf.append("\r\n"); + buf.append(getBodyAsString()); + return buf.toString(); + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponseParser.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponseParser.java new file mode 100755 index 00000000..9f5ad154 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/heartbeat/client/SimpleHttpResponseParser.java @@ -0,0 +1,154 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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.alibaba.csp.sentinel.transport.heartbeat.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + *

    + * The parser provides functionality to parse raw bytes HTTP response to a {@link SimpleHttpResponse}. + *

    + *

    + * Note that this is a very NAIVE parser, {@code Content-Length} must be specified in the + * HTTP response header, otherwise, the body will be dropped. All other body type such as + * {@code Transfer-Encoding: chunked}, {@code Transfer-Encoding: deflate} are not supported. + *

    + * + * @author leyou + */ +public class SimpleHttpResponseParser { + + private static final int MAX_BODY_SIZE = 1024 * 1024 * 4; + private byte[] buf; + + public SimpleHttpResponseParser(int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException("maxSize must > 0"); + } + this.buf = new byte[maxSize]; + } + + public SimpleHttpResponseParser() { + this(1024 * 4); + } + + /** + * Parse bytes from an input stream to a {@link SimpleHttpResponse}. + * + * @param in input stream + * @return parsed HTTP response entity + * @throws IOException when an IO error occurs + */ + public SimpleHttpResponse parse(InputStream in) throws IOException { + int bg = 0; + int len; + String statusLine = null; + Map headers = new HashMap(); + Charset charset = Charset.forName("utf-8"); + int contentLength = -1; + SimpleHttpResponse response; + while (true) { + if (bg >= buf.length) { + throw new IndexOutOfBoundsException("buf index out of range: " + bg + ", buf.length=" + buf.length); + } + if ((len = in.read(buf, bg, buf.length - bg)) > 0) { + bg += len; + len = bg; + int idx; + int parseBg = 0; + while ((idx = indexOfCRLF(parseBg, len)) >= 0) { + String line = new String(buf, parseBg, idx - parseBg, charset); + parseBg = idx + 2; + if (statusLine == null) { + statusLine = line; + } else { + if (line.isEmpty()) { + //When the `Content-Length` is absent, parse the rest of the bytes as body directly. + //if (contentLength == -1) { + // contentLength = MAX_BODY_SIZE; + //} + + // Parse HTTP body. + // When the `Content-Length` is absent, drop the body, return directly. + response = new SimpleHttpResponse(statusLine, headers); + if (contentLength <= 0) { + return response; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(1024); + // `Content-Length` is not equal to exact length. + if (contentLength < len - parseBg) { + throw new IllegalStateException("Invalid content length: " + contentLength); + } + out.write(buf, parseBg, len - parseBg); + if (out.size() > MAX_BODY_SIZE) { + throw new IllegalStateException( + "Request body is too big, limit size is " + MAX_BODY_SIZE); + } + int cap = Math.min(contentLength - out.size(), buf.length); + while (cap > 0 && (len = in.read(buf, 0, cap)) > 0) { + out.write(buf, 0, len); + cap = Math.min(contentLength - out.size(), buf.length); + } + response.setBody(out.toByteArray()); + return response; + } else if (!line.trim().isEmpty()) { + // Parse HTTP header. + int idx2 = line.indexOf(":"); + String key = line.substring(0, idx2).trim(); + String value = line.substring(idx2 + 1).trim(); + headers.put(key, value); + if ("Content-Length".equalsIgnoreCase(key)) { + contentLength = Integer.parseInt(value); + } + } + } + } + // Move remaining bytes to the beginning. + if (parseBg != 0) { + System.arraycopy(buf, parseBg, buf, 0, len - parseBg); + } + bg = len - parseBg; + } else { + break; + } + } + return null; + } + + /** + * Get the index of CRLF separator. + * + * @param bg begin offset + * @param ed end offset + * @return the index, or {@code -1} if no CRLF is found + */ + private int indexOfCRLF(int bg, int ed) { + if (ed - bg < 2) { + return -1; + } + for (int i = bg; i < ed - 1; i++) { + if (buf[i] == '\r' && buf[i + 1] == '\n') { + return i; + } + } + return -1; + } +} \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter b/sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter new file mode 100755 index 00000000..b5790b50 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.CommandCenter @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender b/sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender new file mode 100755 index 00000000..190dc363 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.transport.HeartbeatSender @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender \ No newline at end of file