github.com/iotexproject/iotex-core@v1.14.1-rc1/tools/minicluster/minicluster.go (about) 1 // Copyright (c) 2019 IoTeX Foundation 2 // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability 3 // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. 4 // This source code is governed by Apache License 2.0 that can be found in the LICENSE file. 5 6 // usage: make minicluster 7 8 package main 9 10 import ( 11 "context" 12 "flag" 13 "fmt" 14 "math" 15 "math/big" 16 "math/rand" 17 "os" 18 "sync" 19 "time" 20 21 "github.com/iotexproject/go-pkgs/cache/ttl" 22 "github.com/iotexproject/go-pkgs/crypto" 23 "github.com/iotexproject/iotex-proto/golang/iotexapi" 24 "github.com/iotexproject/iotex-proto/golang/iotextypes" 25 "go.uber.org/zap" 26 "google.golang.org/grpc" 27 28 "github.com/iotexproject/iotex-core/action/protocol" 29 "github.com/iotexproject/iotex-core/action/protocol/rewarding" 30 "github.com/iotexproject/iotex-core/blockchain" 31 "github.com/iotexproject/iotex-core/blockchain/genesis" 32 "github.com/iotexproject/iotex-core/config" 33 "github.com/iotexproject/iotex-core/pkg/log" 34 "github.com/iotexproject/iotex-core/pkg/probe" 35 "github.com/iotexproject/iotex-core/pkg/unit" 36 "github.com/iotexproject/iotex-core/pkg/util/fileutil" 37 "github.com/iotexproject/iotex-core/server/itx" 38 "github.com/iotexproject/iotex-core/state/factory" 39 "github.com/iotexproject/iotex-core/testutil" 40 "github.com/iotexproject/iotex-core/tools/executiontester/assetcontract" 41 bc "github.com/iotexproject/iotex-core/tools/executiontester/blockchain" 42 "github.com/iotexproject/iotex-core/tools/util" 43 ) 44 45 const ( 46 _numNodes = 4 47 _numAdmins = 2 48 ) 49 50 func main() { 51 // timeout indicates the duration of running nightly build in seconds. Default is 300 52 var timeout int 53 // aps indicates how many actions to be injected in one second. Default is 0 54 var aps float64 55 // smart contract deployment data. Default is "608060405234801561001057600080fd5b506102f5806100206000396000f3006080604052600436106100615763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632885ad2c8114610066578063797d9fbd14610070578063cd5e3c5d14610091578063d0e30db0146100b8575b600080fd5b61006e6100c0565b005b61006e73ffffffffffffffffffffffffffffffffffffffff600435166100cb565b34801561009d57600080fd5b506100a6610159565b60408051918252519081900360200190f35b61006e610229565b6100c9336100cb565b565b60006100d5610159565b6040805182815290519192507fbae72e55df73720e0f671f4d20a331df0c0dc31092fda6c573f35ff7f37f283e919081900360200190a160405173ffffffffffffffffffffffffffffffffffffffff8316906305f5e100830280156108fc02916000818181858888f19350505050158015610154573d6000803e3d6000fd5b505050565b604080514460208083019190915260001943014082840152825180830384018152606090920192839052815160009360059361021a9360029391929182918401908083835b602083106101bd5780518252601f19909201916020918201910161019e565b51815160209384036101000a600019018019909216911617905260405191909301945091925050808303816000865af11580156101fe573d6000803e3d6000fd5b5050506040513d602081101561021357600080fd5b5051610261565b81151561022357fe5b06905090565b60408051348152905133917fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c919081900360200190a2565b600080805b60208110156102c25780600101602060ff160360080260020a848260208110151561028d57fe5b7f010000000000000000000000000000000000000000000000000000000000000091901a810204029190910190600101610266565b50929150505600a165627a7a72305820a426929891673b0a04d7163b60113d28e7d0f48ea667680ba48126c182b872c10029" 56 var deployExecData string 57 // smart contract interaction data. Default is "d0e30db0" 58 var interactExecData string 59 // switch of fp token smart contract test. Default is false 60 var testFpToken bool 61 62 flag.IntVar(&timeout, "timeout", 100, "duration of running nightly build") 63 flag.Float64Var(&aps, "aps", 1, "actions to be injected per second") 64 flag.StringVar(&deployExecData, "deploy-data", "608060405234801561001057600080fd5b506102f5806100206000396000f3006080604052600436106100615763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632885ad2c8114610066578063797d9fbd14610070578063cd5e3c5d14610091578063d0e30db0146100b8575b600080fd5b61006e6100c0565b005b61006e73ffffffffffffffffffffffffffffffffffffffff600435166100cb565b34801561009d57600080fd5b506100a6610159565b60408051918252519081900360200190f35b61006e610229565b6100c9336100cb565b565b60006100d5610159565b6040805182815290519192507fbae72e55df73720e0f671f4d20a331df0c0dc31092fda6c573f35ff7f37f283e919081900360200190a160405173ffffffffffffffffffffffffffffffffffffffff8316906305f5e100830280156108fc02916000818181858888f19350505050158015610154573d6000803e3d6000fd5b505050565b604080514460208083019190915260001943014082840152825180830384018152606090920192839052815160009360059361021a9360029391929182918401908083835b602083106101bd5780518252601f19909201916020918201910161019e565b51815160209384036101000a600019018019909216911617905260405191909301945091925050808303816000865af11580156101fe573d6000803e3d6000fd5b5050506040513d602081101561021357600080fd5b5051610261565b81151561022357fe5b06905090565b60408051348152905133917fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c919081900360200190a2565b600080805b60208110156102c25780600101602060ff160360080260020a848260208110151561028d57fe5b7f010000000000000000000000000000000000000000000000000000000000000091901a810204029190910190600101610266565b50929150505600a165627a7a72305820a426929891673b0a04d7163b60113d28e7d0f48ea667680ba48126c182b872c10029", 65 "smart contract deployment data") 66 flag.StringVar(&interactExecData, "interact-data", "d0e30db0", "smart contract interaction data") 67 flag.BoolVar(&testFpToken, "fp-token", false, "switch of fp token smart contract test") 68 flag.Parse() 69 70 // path of config file containing all the public/private key paris of addresses getting transfers 71 // from Creator in genesis block 72 injectorConfigPath := "./tools/minicluster/gentsfaddrs.yaml" 73 74 chainAddrs, err := util.LoadAddresses(injectorConfigPath, uint32(1)) 75 if err != nil { 76 log.L().Fatal("Failed to load addresses from config path", zap.Error(err)) 77 } 78 admins := chainAddrs[len(chainAddrs)-_numAdmins:] 79 delegates := chainAddrs[:len(chainAddrs)-_numAdmins] 80 81 dbFilePaths := make([]string, 0) 82 //a flag to indicate whether the DB files should be cleaned up upon completion of the minicluster. 83 deleteDBFiles := false 84 85 // Set mini-cluster configurations 86 configs := make([]config.Config, _numNodes) 87 for i := 0; i < _numNodes; i++ { 88 chainDBPath := fmt.Sprintf("./chain%d.db", i+1) 89 dbFilePaths = append(dbFilePaths, chainDBPath) 90 trieDBPath := fmt.Sprintf("./trie%d.db", i+1) 91 dbFilePaths = append(dbFilePaths, trieDBPath) 92 indexDBPath := fmt.Sprintf("./index%d.db", i+1) 93 dbFilePaths = append(dbFilePaths, indexDBPath) 94 bloomfilterIndexDBPath := fmt.Sprintf("./bloomfilter.index%d.db", i+1) 95 dbFilePaths = append(dbFilePaths, bloomfilterIndexDBPath) 96 consensusDBPath := fmt.Sprintf("./consensus%d.db", i+1) 97 dbFilePaths = append(dbFilePaths, consensusDBPath) 98 systemLogDBPath := fmt.Sprintf("./systemlog%d.db", i+1) 99 dbFilePaths = append(dbFilePaths, systemLogDBPath) 100 candidateIndexDBPath := fmt.Sprintf("./candidate.index%d.db", i+1) 101 dbFilePaths = append(dbFilePaths, candidateIndexDBPath) 102 networkPort := config.Default.Network.Port + i 103 apiPort := config.Default.API.GRPCPort + i 104 web3APIPort := config.Default.API.HTTPPort + i 105 web3SocketPort := config.Default.API.WebSocketPort + i 106 HTTPAdminPort := config.Default.System.HTTPAdminPort + i 107 config := newConfig(chainAddrs[i].PriKey, networkPort, apiPort, web3APIPort, web3SocketPort, HTTPAdminPort) 108 config.Chain.ChainDBPath = chainDBPath 109 config.Chain.TrieDBPatchFile = "" 110 config.Chain.TrieDBPath = trieDBPath 111 config.Chain.IndexDBPath = indexDBPath 112 config.Chain.BloomfilterIndexDBPath = bloomfilterIndexDBPath 113 config.Chain.CandidateIndexDBPath = candidateIndexDBPath 114 config.Consensus.RollDPoS.ConsensusDBPath = consensusDBPath 115 config.System.SystemLogDBPath = systemLogDBPath 116 if i == 0 { 117 config.Network.BootstrapNodes = []string{} 118 config.Network.MasterKey = "bootnode" 119 } 120 config.Genesis.AleutianBlockHeight = 1 121 config.Genesis.PacificBlockHeight = 1 122 configs[i] = config 123 } 124 125 // Create mini-cluster 126 svrs := make([]*itx.Server, _numNodes) 127 for i := 0; i < _numNodes; i++ { 128 svr, err := itx.NewServer(configs[i]) 129 if err != nil { 130 log.L().Fatal("Failed to create server.", zap.Error(err)) 131 } 132 svrs[i] = svr 133 } 134 defer func() { 135 if !deleteDBFiles { 136 return 137 } 138 for _, dbFilePath := range dbFilePaths { 139 if fileutil.FileExists(dbFilePath) && os.RemoveAll(dbFilePath) != nil { 140 log.L().Error("Failed to delete db file") 141 } 142 } 143 }() 144 145 // Start mini-cluster 146 for i := 0; i < _numNodes; i++ { 147 ctx, cancel := context.WithCancel(context.Background()) 148 defer cancel() 149 go itx.StartServer(ctx, svrs[i], probe.New(7788+i), configs[i]) 150 } 151 152 // target address for grpc connection. Default is "127.0.0.1:14014" 153 grpcAddr := "127.0.0.1:14014" 154 155 grpcctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 156 defer cancel() 157 conn, err := grpc.DialContext(grpcctx, grpcAddr, grpc.WithBlock(), grpc.WithInsecure()) 158 if err != nil { 159 log.L().Error("Failed to connect to API server.") 160 } 161 defer conn.Close() 162 163 client := iotexapi.NewAPIServiceClient(conn) 164 165 counter, err := util.InitCounter(client, chainAddrs) 166 if err != nil { 167 log.L().Fatal("Failed to initialize nonce counter", zap.Error(err)) 168 } 169 170 // Inject actions to first node 171 if aps > 0 { 172 // transfer gas limit. Default is 1000000 173 transferGasLimit := 1000000 174 // transfer gas price. Default is 10 175 transferGasPrice := unit.Qev 176 // transfer payload. Default is "" 177 transferPayload := "" 178 // vote gas limit. Default is 1000000 179 voteGasLimit := 1000000 180 // vote gas price. Default is 10 181 voteGasPrice := unit.Qev 182 // execution amount. Default is 0 183 executionAmount := 0 184 // execution gas limit. Default is 1200000 185 executionGasLimit := 1200000 186 // execution gas price. Default is 10 187 executionGasPrice := unit.Qev 188 // maximum number of rpc retries. Default is 5 189 retryNum := 5 190 // sleeping period between two consecutive rpc retries in seconds. Default is 1 191 retryInterval := 1 192 // reset interval indicates the interval to reset nonce counter in seconds. Default is 60 193 resetInterval := 60 194 // fpTotal indicates the total amount value of a fp token 195 fpTotal := int64(20000) 196 // fpRisk indicates the risk amount value of a fp token 197 fpRisk := int64(1000) 198 199 d := time.Duration(timeout) * time.Second 200 201 // First deploy a user specified smart contract which can be interacted by injected executions 202 eHash, err := util.DeployContract(client, counter, delegates, executionGasLimit, executionGasPrice, 203 deployExecData, retryNum, retryInterval) 204 if err != nil { 205 log.L().Fatal("Failed to deploy smart contract", zap.Error(err)) 206 } 207 // Wait until the smart contract is successfully deployed 208 var ( 209 receipt *iotextypes.Receipt 210 as = svrs[0].APIServer(1) 211 ) 212 if err := testutil.WaitUntil(100*time.Millisecond, 60*time.Second, func() (bool, error) { 213 receipt, err = util.GetReceiptByAction(as.CoreService(), eHash) 214 return receipt != nil, nil 215 }); err != nil { 216 log.L().Fatal("Failed to get receipt of execution deployment", zap.Error(err)) 217 } 218 contract := receipt.ContractAddress 219 220 var fpToken bc.FpToken 221 var fpContract string 222 var debtor *util.AddressKey 223 var creditor *util.AddressKey 224 if testFpToken { 225 // Deploy asset smart contracts 226 ret, err := assetcontract.StartContracts(configs[0]) 227 if err != nil { 228 log.L().Fatal("Failed to deploy asset contracts.", zap.Error(err)) 229 } 230 fpToken = ret.FpToken 231 // Randomly pick two accounts from delegate list as fp_token debtor and creditor 232 first := rand.Intn(len(admins)) 233 second := first 234 for second == first { 235 second = rand.Intn(len(admins)) 236 } 237 debtor = admins[first] 238 creditor = admins[second] 239 240 // Create fp token 241 assetID := assetcontract.GenerateAssetID() 242 open := time.Now().Unix() 243 exp := open + 100000 244 245 if _, err := fpToken.CreateToken(assetID, debtor.EncodedAddr, creditor.EncodedAddr, fpTotal, fpRisk, open, 246 exp); err != nil { 247 log.L().Fatal("Failed to create fp token", zap.Error(err)) 248 } 249 250 fpContract, err = fpToken.TokenAddress(assetID) 251 if err != nil { 252 log.L().Fatal("Failed to get token contract address", zap.Error(err)) 253 } 254 255 // Transfer full amount from debtor to creditor 256 debtorPriKey := debtor.PriKey.HexString() 257 if _, err := fpToken.Transfer(fpContract, debtor.EncodedAddr, debtorPriKey, 258 creditor.EncodedAddr, fpTotal); err != nil { 259 log.L().Fatal("Failed to transfer total amount from debtor to creditor", zap.Error(err)) 260 } 261 262 // Transfer amount of risk from creditor to contract 263 creditorPriKey := creditor.PriKey.HexString() 264 if _, err := fpToken.RiskLock(fpContract, creditor.EncodedAddr, creditorPriKey, 265 fpRisk); err != nil { 266 log.L().Fatal("Failed to transfer amount of risk from creditor to contract", zap.Error(err)) 267 } 268 } 269 270 expectedBalancesMap := util.GetAllBalanceMap(client, chainAddrs) 271 pendingActionMap, _ := ttl.NewCache(ttl.EvictOnErrorOption()) 272 273 log.L().Info("Start action injections.") 274 275 wg := &sync.WaitGroup{} 276 util.InjectByAps(wg, aps, counter, transferGasLimit, transferGasPrice, transferPayload, voteGasLimit, 277 voteGasPrice, contract, executionAmount, executionGasLimit, executionGasPrice, interactExecData, fpToken, 278 fpContract, debtor, creditor, client, admins, delegates, d, retryNum, retryInterval, resetInterval, 279 expectedBalancesMap, as.CoreService(), pendingActionMap) 280 wg.Wait() 281 282 err = testutil.WaitUntil(100*time.Millisecond, 60*time.Second, func() (bool, error) { 283 empty, err := util.CheckPendingActionList( 284 as.CoreService(), 285 pendingActionMap, 286 expectedBalancesMap, 287 ) 288 if err != nil { 289 log.L().Error(err.Error()) 290 return false, err 291 } 292 return empty, nil 293 }) 294 295 totalPendingActions := 0 296 pendingActionMap.Range(func(selphash, vi interface{}) error { 297 totalPendingActions++ 298 return nil 299 }) 300 301 if err != nil { 302 log.L().Error("Not all actions are settled") 303 } 304 305 chains := make([]blockchain.Blockchain, _numNodes) 306 sfs := make([]factory.Factory, _numNodes) 307 stateHeights := make([]uint64, _numNodes) 308 bcHeights := make([]uint64, _numNodes) 309 idealHeight := make([]uint64, _numNodes) 310 311 var netTimeout int 312 var minTimeout int 313 314 for i := 0; i < _numNodes; i++ { 315 chains[i] = svrs[i].ChainService(configs[i].Chain.ID).Blockchain() 316 sfs[i] = svrs[i].ChainService(configs[i].Chain.ID).StateFactory() 317 318 stateHeights[i], err = sfs[i].Height() 319 if err != nil { 320 log.S().Errorf("Node %d: Can not get State height", i) 321 } 322 bcHeights[i] = chains[i].TipHeight() 323 minTimeout = int(configs[i].Consensus.RollDPoS.Delay/time.Second - configs[i].Genesis.BlockInterval/time.Second) 324 netTimeout = 0 325 if timeout > minTimeout { 326 netTimeout = timeout - minTimeout 327 } 328 idealHeight[i] = uint64((time.Duration(netTimeout) * time.Second) / configs[i].Genesis.BlockInterval) 329 330 log.S().Infof("Node#%d blockchain height: %d", i, bcHeights[i]) 331 log.S().Infof("Node#%d state height: %d", i, stateHeights[i]) 332 log.S().Infof("Node#%d ideal height: %d", i, idealHeight[i]) 333 334 if bcHeights[i] != stateHeights[i] { 335 log.S().Errorf("Node#%d: State height does not match blockchain height", i) 336 } 337 if bcHeights[i] < idealHeight[i] { 338 log.S().Errorf("blockchain in Node#%d is behind the expected height", i) 339 } 340 } 341 342 for i := 0; i < _numNodes; i++ { 343 for j := i + 1; j < _numNodes; j++ { 344 if math.Abs(float64(bcHeights[i]-bcHeights[j])) > 1 { 345 log.S().Errorf("blockchain in Node#%d and blockchain in Node#%d are not sync", i, j) 346 } else { 347 log.S().Infof("blockchain in Node#%d and blockchain in Node#%d are sync", i, j) 348 } 349 } 350 } 351 352 m := util.GetAllBalanceMap(client, chainAddrs) 353 balanceCheckPass := true 354 for k, v := range m { 355 if len(expectedBalancesMap) != 0 && v.Cmp(expectedBalancesMap[k]) != 0 { 356 balanceCheckPass = false 357 log.S().Info("Balance mismatch on account ", k) 358 log.S().Info("Real balance: ", v.String(), " Expected balance: ", expectedBalancesMap[k].String()) 359 360 } 361 } 362 if balanceCheckPass { 363 log.S().Info("Balance Check PASS") 364 } else { 365 log.S().Fatal("Balance Mismatch") 366 } 367 368 log.S().Info("Total Transfer created: ", util.GetTotalTsfCreated()) 369 log.S().Info("Total Transfer inject through grpc: ", util.GetTotalTsfSentToAPI()) 370 log.S().Info("Total Transfer succeed: ", util.GetTotalTsfSucceeded()) 371 log.S().Info("Total Transfer failed: ", util.GetTotalTsfFailed()) 372 log.S().Info("Total pending actions: ", totalPendingActions) 373 374 if testFpToken { 375 // Check fp token asset balance 376 debtorBalance, err := fpToken.ReadValue(fpContract, "70a08231", debtor.EncodedAddr) 377 if err != nil { 378 log.S().Error("Failed to get debtor's asset balance.", zap.Error(err)) 379 } 380 log.L().Info("Debtor's asset balance: ", zap.Int64("balance", debtorBalance)) 381 382 creditorBalance, err := fpToken.ReadValue(fpContract, "70a08231", creditor.EncodedAddr) 383 if err != nil { 384 log.S().Error("Failed to get creditor's asset balance.", zap.Error(err)) 385 } 386 log.L().Info("Creditor's asset balance: ", zap.Int64("balance", creditorBalance)) 387 388 if debtorBalance+creditorBalance != fpTotal-fpRisk { 389 log.S().Error("Sum of asset balance is incorrect.") 390 return 391 } 392 393 log.S().Info("Fp token transfer test pass!") 394 } 395 396 registries := make([]*protocol.Registry, _numNodes) 397 for i := 0; i < _numNodes; i++ { 398 registries[i] = svrs[i].ChainService(configs[i].Chain.ID).Registry() 399 400 ctx := protocol.WithBlockCtx(context.Background(), protocol.BlockCtx{ 401 BlockHeight: bcHeights[i], 402 }) 403 ctx = genesis.WithGenesisContext( 404 protocol.WithRegistry(ctx, registries[i]), 405 chains[i].Genesis(), 406 ) 407 ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) 408 409 rp := rewarding.FindProtocol(registries[i]) 410 if rp == nil { 411 log.S().Fatal("rolldpos is not registered.") 412 } 413 414 blockReward, err := rp.BlockReward(ctx, sfs[i]) 415 if err != nil { 416 log.S().Fatal("Failed to get block reward.", zap.Error(err)) 417 } 418 if blockReward == configs[i].Genesis.BlockReward() { 419 log.S().Fatal("actual block reward is incorrect.") 420 } 421 422 epochReward, err := rp.EpochReward(ctx, sfs[i]) 423 if err != nil { 424 log.S().Fatal("Failed to get epoch reward.", zap.Error(err)) 425 } 426 if epochReward == configs[i].Genesis.AleutianEpochReward() { 427 log.S().Fatal("actual epoch reward is incorrect.") 428 } 429 } 430 431 deleteDBFiles = true 432 } 433 } 434 435 func newConfig( 436 producerPriKey crypto.PrivateKey, 437 networkPort, 438 apiPort int, 439 web3APIPort int, 440 webSocketPort int, 441 HTTPAdminPort int, 442 ) config.Config { 443 cfg := config.Default 444 445 cfg.Plugins[config.GatewayPlugin] = true 446 cfg.Chain.EnableAsyncIndexWrite = false 447 448 cfg.System.HTTPAdminPort = HTTPAdminPort 449 cfg.Network.Port = networkPort 450 cfg.Network.BootstrapNodes = []string{"/ip4/127.0.0.1/tcp/4689/ipfs/12D3KooWJwW6pUpTkxPTMv84RPLPMQVEAjZ6fvJuX4oZrvW5DAGQ"} 451 452 cfg.Chain.ID = 1 453 cfg.Chain.ProducerPrivKey = producerPriKey.HexString() 454 455 cfg.ActPool.MinGasPriceStr = big.NewInt(0).String() 456 457 cfg.Consensus.Scheme = config.RollDPoSScheme 458 cfg.Consensus.RollDPoS.FSM.UnmatchedEventInterval = 2400 * time.Millisecond 459 cfg.Consensus.RollDPoS.FSM.AcceptBlockTTL = 1800 * time.Millisecond 460 cfg.Consensus.RollDPoS.FSM.AcceptProposalEndorsementTTL = 1800 * time.Millisecond 461 cfg.Consensus.RollDPoS.FSM.AcceptLockEndorsementTTL = 1800 * time.Millisecond 462 cfg.Consensus.RollDPoS.FSM.CommitTTL = 600 * time.Millisecond 463 cfg.Consensus.RollDPoS.FSM.EventChanSize = 100000 464 cfg.Consensus.RollDPoS.ToleratedOvertime = 1200 * time.Millisecond 465 cfg.Consensus.RollDPoS.Delay = 6 * time.Second 466 467 cfg.API.GRPCPort = apiPort 468 cfg.API.HTTPPort = web3APIPort 469 cfg.API.WebSocketPort = webSocketPort 470 471 cfg.Genesis.BlockInterval = 6 * time.Second 472 cfg.Genesis.Blockchain.NumSubEpochs = 2 473 cfg.Genesis.Blockchain.NumDelegates = _numNodes 474 cfg.Genesis.Blockchain.TimeBasedRotation = true 475 cfg.Genesis.Delegates = cfg.Genesis.Delegates[3 : _numNodes+3] 476 cfg.Genesis.EnableGravityChainVoting = false 477 cfg.Genesis.PollMode = "lifeLong" 478 479 return cfg 480 }