PageRenderTime 63ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/mysql/spec/mysql_node_spec.rb

https://github.com/Abadasoft/vcap-services
Ruby | 666 lines | 592 code | 46 blank | 28 comment | 45 complexity | 1ee6a5ab9e92d4366d04a301660cccae MD5 | raw file
  1. # Copyright (c) 2009-2011 VMware, Inc.
  2. $:.unshift(File.dirname(__FILE__))
  3. require 'spec_helper'
  4. require 'mysql_service/node'
  5. require 'mysql_service/mysql_error'
  6. require 'mysql2'
  7. require 'yajl'
  8. module VCAP
  9. module Services
  10. module Mysql
  11. class Node
  12. attr_reader :pool, :logger, :capacity, :provision_served, :binding_served
  13. end
  14. end
  15. end
  16. end
  17. module VCAP
  18. module Services
  19. module Mysql
  20. class MysqlError
  21. attr_reader :error_code
  22. end
  23. end
  24. end
  25. end
  26. describe "Mysql server node" do
  27. include VCAP::Services::Mysql
  28. before :all do
  29. @opts = getNodeTestConfig
  30. @opts.freeze
  31. # Setup code must be wrapped in EM.run
  32. EM.run do
  33. @node = Node.new(@opts)
  34. EM.add_timer(1) { EM.stop }
  35. end
  36. end
  37. before :each do
  38. @default_plan = "free"
  39. @default_opts = "default"
  40. @test_dbs = {}# for cleanup
  41. # Create one db be default
  42. @db = @node.provision(@default_plan)
  43. @db.should_not == nil
  44. @db["name"].should be
  45. @db["host"].should be
  46. @db["host"].should == @db["hostname"]
  47. @db["port"].should be
  48. @db["user"].should == @db["username"]
  49. @db["password"].should be
  50. @test_dbs[@db] = []
  51. end
  52. it "should connect to mysql database" do
  53. EM.run do
  54. expect {@node.pool.with_connection{|connection| connection.query("SELECT 1")}}.should_not raise_error
  55. EM.stop
  56. end
  57. end
  58. it "should report inconsistency between mysql and local db" do
  59. EM.run do
  60. name, user = @db["name"], @db["user"]
  61. @node.pool.with_connection do |conn|
  62. conn.query("delete from db where db='#{name}' and user='#{user}'")
  63. end
  64. result = @node.check_db_consistency
  65. result.include?([name, user]).should == true
  66. EM.stop
  67. end
  68. end
  69. it "should provison a database with correct credential" do
  70. EM.run do
  71. @db.should be_instance_of Hash
  72. conn = connect_to_mysql(@db)
  73. expect {conn.query("SELECT 1")}.should_not raise_error
  74. EM.stop
  75. end
  76. end
  77. it "should calculate both table and index as database size" do
  78. EM.run do
  79. conn = connect_to_mysql(@db)
  80. # should calculate table size
  81. conn.query("CREATE TABLE test(id INT)")
  82. conn.query("INSERT INTO test VALUE(10)")
  83. conn.query("INSERT INTO test VALUE(20)")
  84. table_size = @node.dbs_size(conn)[@db["name"]]
  85. table_size.should > 0
  86. # should also calculate index size
  87. conn.query("CREATE INDEX id_index on test(id)")
  88. all_size = @node.dbs_size(conn)[@db["name"]]
  89. all_size.should > table_size
  90. EM.stop
  91. end
  92. end
  93. it "should enforce database size quota" do
  94. EM.run do
  95. opts = @opts.dup
  96. # reduce storage quota to 4KB.
  97. opts[:max_db_size] = 4.0/1024
  98. node = VCAP::Services::Mysql::Node.new(opts)
  99. EM.add_timer(1) do
  100. binding = node.bind(@db["name"], @default_opts)
  101. @test_dbs[@db] << binding
  102. conn = connect_to_mysql(binding)
  103. conn.query("create table test(data text)")
  104. c = [('a'..'z'),('A'..'Z')].map{|i| Array(i)}.flatten
  105. content = (0..5000).map{ c[rand(c.size)] }.join
  106. conn.query("insert into test value('#{content}')")
  107. EM.add_timer(3) do
  108. expect {conn.query('SELECT 1')}.should raise_error
  109. conn.close
  110. conn = connect_to_mysql(binding)
  111. # write privilege should be rovoked.
  112. expect{ conn.query("insert into test value('test')")}.should raise_error(Mysql2::Error)
  113. conn2 = connect_to_mysql(@db)
  114. expect{ conn.query("insert into test value('test')")}.should raise_error(Mysql2::Error)
  115. conn.query("delete from test")
  116. EM.add_timer(3) do
  117. expect {conn.query('SELECT 1')}.should raise_error
  118. conn.close
  119. conn = connect_to_mysql(binding)
  120. # write privilege should restore
  121. expect{ conn.query("insert into test value('test')")}.should_not raise_error
  122. EM.stop
  123. end
  124. end
  125. end
  126. end
  127. end
  128. it "should able to handle orphan instances when enforce storage quota." do
  129. begin
  130. # forge an orphan instance, which is not exist in mysql
  131. klass = VCAP::Services::Mysql::Node::ProvisionedService
  132. DataMapper.setup(:default, @opts[:local_db])
  133. DataMapper::auto_upgrade!
  134. service = klass.new
  135. service.name = 'test-'+ UUIDTools::UUID.random_create.to_s
  136. service.user = "test"
  137. service.password = "test"
  138. service.plan = 1
  139. if not service.save
  140. raise "Failed to forge orphan instance: #{service.errors.inspect}"
  141. end
  142. EM.run do
  143. expect { @node.enforce_storage_quota }.should_not raise_error
  144. EM.stop
  145. end
  146. ensure
  147. service.destroy
  148. end
  149. end
  150. it "should return correct instances & binding list" do
  151. EM.run do
  152. before_ins_list = @node.all_instances_list
  153. plan = "free"
  154. tmp_db = @node.provision(plan)
  155. @test_dbs[tmp_db] = []
  156. after_ins_list = @node.all_instances_list
  157. before_ins_list << tmp_db["name"]
  158. (before_ins_list.sort == after_ins_list.sort).should be_true
  159. before_bind_list = @node.all_bindings_list
  160. tmp_credential = @node.bind(tmp_db["name"], @default_opts)
  161. @test_dbs[tmp_db] << tmp_credential
  162. after_bind_list = @node.all_bindings_list
  163. before_bind_list << tmp_credential
  164. a,b = [after_bind_list,before_bind_list].map do |list|
  165. list.map{|item| item["username"]}.sort
  166. end
  167. (a == b).should be_true
  168. EM.stop
  169. end
  170. end
  171. it "should not create db or send response if receive a malformed request" do
  172. EM.run do
  173. @node.pool.with_connection do |connection|
  174. db_num = connection.query("show databases;").count
  175. mal_plan = "not-a-plan"
  176. db = nil
  177. expect {
  178. db = @node.provision(mal_plan)
  179. }.should raise_error(MysqlError, /Invalid plan .*/)
  180. db.should == nil
  181. db_num.should == connection.query("show databases;").count
  182. end
  183. EM.stop
  184. end
  185. end
  186. it "should support over provisioning" do
  187. EM.run do
  188. opts = @opts.dup
  189. opts[:capacity] = 10
  190. opts[:max_db_size] = 20
  191. node = VCAP::Services::Mysql::Node.new(opts)
  192. EM.add_timer(1) do
  193. expect {
  194. db = node.provision(@default_plan)
  195. @test_dbs[db] = []
  196. }.should_not raise_error
  197. EM.stop
  198. end
  199. end
  200. end
  201. it "should not allow old credential to connect if service is unprovisioned" do
  202. EM.run do
  203. conn = connect_to_mysql(@db)
  204. expect {conn.query("SELECT 1")}.should_not raise_error
  205. msg = Yajl::Encoder.encode(@db)
  206. @node.unprovision(@db["name"], [])
  207. expect {connect_to_mysql(@db)}.should raise_error
  208. error = nil
  209. EM.stop
  210. end
  211. end
  212. it "should return proper error if unprovision a not existing instance" do
  213. EM.run do
  214. expect {
  215. @node.unprovision("not-existing", [])
  216. }.should raise_error(MysqlError, /Mysql configuration .* not found/)
  217. # nil input handle
  218. @node.unprovision(nil, []).should == nil
  219. EM.stop
  220. end
  221. end
  222. it "should not be possible to access one database using null or wrong credential" do
  223. EM.run do
  224. plan = "free"
  225. db2 = @node.provision(plan)
  226. @test_dbs[db2] = []
  227. fake_creds = []
  228. 3.times {fake_creds << @db.clone}
  229. # try to login other's db
  230. fake_creds[0]["name"] = db2["name"]
  231. # try to login using null credential
  232. fake_creds[1]["password"] = nil
  233. # try to login using root account
  234. fake_creds[2]["user"] = "root"
  235. fake_creds.each do |creds|
  236. expect{connect_to_mysql(creds)}.should raise_error
  237. end
  238. EM.stop
  239. end
  240. end
  241. it "should kill long transaction" do
  242. if @opts[:max_long_tx] > 0 and (@node.check_innodb_plugin)
  243. EM.run do
  244. opts = @opts.dup
  245. # reduce max_long_tx to accelerate test
  246. opts[:max_long_tx] = 1
  247. node = VCAP::Services::Mysql::Node.new(opts)
  248. EM.add_timer(1) do
  249. conn = connect_to_mysql(@db)
  250. # prepare a transaction and not commit
  251. conn.query("create table a(id int) engine=innodb")
  252. conn.query("insert into a value(10)")
  253. conn.query("begin")
  254. conn.query("select * from a for update")
  255. EM.add_timer(opts[:max_long_tx] * 5) {
  256. expect {conn.query("select * from a for update")}.should raise_error(Mysql2::Error)
  257. conn.close
  258. EM.stop
  259. }
  260. end
  261. end
  262. else
  263. pending "long transaction killer is disabled."
  264. end
  265. end
  266. it "should kill long queries" do
  267. pending "Disable for non-Percona server since the test behavior varies on regular Mysql server." unless @node.is_percona_server?
  268. EM.run do
  269. db = @node.provision(@default_plan)
  270. @test_dbs[db] = []
  271. opts = @opts.dup
  272. opts[:max_long_query] = 1
  273. conn = connect_to_mysql(db)
  274. node = VCAP::Services::Mysql::Node.new(opts)
  275. EM.add_timer(1) do
  276. conn.query('create table test(id INT) engine innodb')
  277. conn.query('insert into test value(10)')
  278. conn.query('begin')
  279. # lock table test
  280. conn.query('select * from test where id = 10 for update')
  281. old_counter = node.varz_details[:long_queries_killed]
  282. conn2 = connect_to_mysql(db)
  283. err = nil
  284. t = Thread.new do
  285. begin
  286. # conn2 is blocked by conn, we use lock to simulate long queries
  287. conn2.query("select * from test for update")
  288. rescue => e
  289. err = e
  290. ensure
  291. conn2.close
  292. end
  293. end
  294. EM.add_timer(opts[:max_long_query] * 5){
  295. err.should_not == nil
  296. err.message.should =~ /interrupted/
  297. # counter should also be updated
  298. node.varz_details[:long_queries_killed].should > old_counter
  299. EM.stop
  300. }
  301. end
  302. end
  303. end
  304. it "should create a new credential when binding" do
  305. EM.run do
  306. binding = @node.bind(@db["name"], @default_opts)
  307. binding["name"].should == @db["name"]
  308. binding["host"].should be
  309. binding["host"].should == binding["hostname"]
  310. binding["port"].should be
  311. binding["user"].should == binding["username"]
  312. binding["password"].should be
  313. @test_dbs[@db] << binding
  314. conn = connect_to_mysql(binding)
  315. expect {conn.query("Select 1")}.should_not raise_error
  316. EM.stop
  317. end
  318. end
  319. it "should supply different credentials when binding evoked with the same input" do
  320. EM.run do
  321. binding = @node.bind(@db["name"], @default_opts)
  322. binding2 = @node.bind(@db["name"], @default_opts)
  323. @test_dbs[@db] << binding
  324. @test_dbs[@db] << binding2
  325. binding.should_not == binding2
  326. EM.stop
  327. end
  328. end
  329. it "should delete credential after unbinding" do
  330. EM.run do
  331. binding = @node.bind(@db["name"], @default_opts)
  332. @test_dbs[@db] << binding
  333. conn = nil
  334. expect {conn = connect_to_mysql(binding)}.should_not raise_error
  335. res = @node.unbind(binding)
  336. res.should be true
  337. expect {connect_to_mysql(binding)}.should raise_error
  338. # old session should be killed
  339. expect {conn.query("SELECT 1")}.should raise_error(Mysql2::Error)
  340. EM.stop
  341. end
  342. end
  343. it "should not delete user in credential when unbind 'ancient' instances" do
  344. EM.run do
  345. # Crafting an ancient binding credential which is the same as provision credential
  346. ancient_binding = @db.dup
  347. expect { connect_to_mysql(ancient_binding) }.should_not raise_error
  348. @node.unbind(ancient_binding)
  349. # ancient_binding is still valid after unbind
  350. expect { connect_to_mysql(ancient_binding) }.should_not raise_error
  351. EM.stop
  352. end
  353. end
  354. it "should delete all bindings if service is unprovisioned" do
  355. EM.run do
  356. @default_opts = "default"
  357. bindings = []
  358. 3.times { bindings << @node.bind(@db["name"], @default_opts)}
  359. @test_dbs[@db] = bindings
  360. conn = nil
  361. @node.unprovision(@db["name"], bindings)
  362. bindings.each { |binding| expect {connect_to_mysql(binding)}.should raise_error }
  363. EM.stop
  364. end
  365. end
  366. it "should able to restore database from backup file" do
  367. EM.run do
  368. db = @node.provision(@default_plan)
  369. @test_dbs[db] = []
  370. conn = connect_to_mysql(db)
  371. conn.query("create table test(id INT)")
  372. # backup current db
  373. host, port, user, password = %w(host port user pass).map{|key| @opts[:mysql][key]}
  374. tmp_file = "/tmp/#{db['name']}.sql.gz"
  375. result = `mysqldump -h #{host} -P #{port} -u #{user} --password=#{password} #{db['name']} | gzip > #{tmp_file}`
  376. conn.query("drop table test")
  377. res = conn.query("show tables")
  378. res.count.should == 0
  379. # create a new table which should be deleted after restore
  380. conn.query("create table test2(id int)")
  381. @node.restore(db["name"], "/tmp/").should == true
  382. conn = connect_to_mysql(db)
  383. res = conn.query("show tables")
  384. res.count().should == 1
  385. res.first["Tables_in_#{db['name']}"].should == "test"
  386. EM.stop
  387. end
  388. end
  389. it "should be able to disable an instance" do
  390. EM.run do
  391. bind_cred = @node.bind(@db["name"], @default_opts)
  392. conn = connect_to_mysql(bind_cred)
  393. @test_dbs[@db] << bind_cred
  394. @node.disable_instance(@db, [bind_cred])
  395. # kill existing session
  396. expect { conn.query('SELECT 1')}.should raise_error
  397. expect { conn2.query('SELECT 1')}.should raise_error
  398. # delete user
  399. expect { connect_to_mysql(bind_cred)}.should raise_error
  400. EM.stop
  401. end
  402. end
  403. it "should able to dump instance content to file" do
  404. EM.run do
  405. conn = connect_to_mysql(@db)
  406. conn.query('create table MyTestTable(id int)')
  407. @node.dump_instance(@db, nil, '/tmp').should == true
  408. File.open(File.join("/tmp", "#{@db['name']}.sql")) do |f|
  409. line = f.each_line.find {|line| line =~ /MyTestTable/}
  410. line.should_not be nil
  411. end
  412. EM.stop
  413. end
  414. end
  415. it "should recreate database and user when import instance" do
  416. EM.run do
  417. db = @node.provision(@default_plan)
  418. @test_dbs[db] = []
  419. @node.dump_instance(db, nil , '/tmp')
  420. @node.unprovision(db['name'], [])
  421. @node.import_instance(db, {}, '/tmp', @default_plan).should == true
  422. conn = connect_to_mysql(db)
  423. expect { conn.query('SELECT 1')}.should_not raise_error
  424. EM.stop
  425. end
  426. end
  427. it "should recreate bindings when enable instance" do
  428. EM.run do
  429. db = @node.provision(@default_plan)
  430. @test_dbs[db] = []
  431. binding = @node.bind(db['name'], @default_opts)
  432. @test_dbs[db] << binding
  433. conn = connect_to_mysql(binding)
  434. @node.disable_instance(db, [binding])
  435. expect {conn = connect_to_mysql(binding)}.should raise_error
  436. value = {
  437. "fake_service_id" => {
  438. "credentials" => binding,
  439. "binding_options" => @default_opts,
  440. }
  441. }
  442. result = @node.enable_instance(db, value)
  443. result.should be_instance_of Array
  444. expect {conn = connect_to_mysql(binding)}.should_not raise_error
  445. EM.stop
  446. end
  447. end
  448. it "should retain instance data after node restart" do
  449. EM.run do
  450. node = VCAP::Services::Mysql::Node.new(@opts)
  451. EM.add_timer(1) do
  452. db = node.provision(@default_plan)
  453. @test_dbs[db] = []
  454. conn = connect_to_mysql(db)
  455. conn.query('create table test(id int)')
  456. # simulate we restart the node
  457. node.shutdown
  458. node = VCAP::Services::Mysql::Node.new(@opts)
  459. EM.add_timer(1) do
  460. conn2 = connect_to_mysql(db)
  461. result = conn2.query('show tables')
  462. result.count.should == 1
  463. EM.stop
  464. end
  465. end
  466. end
  467. end
  468. it "should able to generate varz." do
  469. EM.run do
  470. node = VCAP::Services::Mysql::Node.new(@opts)
  471. EM.add_timer(1) do
  472. varz = node.varz_details
  473. varz.should be_instance_of Hash
  474. varz[:queries_since_startup].should >0
  475. varz[:queries_per_second].should >= 0
  476. varz[:database_status].should be_instance_of Array
  477. varz[:max_capacity].should > 0
  478. varz[:available_capacity].should >= 0
  479. varz[:long_queries_killed].should >= 0
  480. varz[:long_transactions_killed].should >= 0
  481. varz[:provision_served].should >= 0
  482. varz[:binding_served].should >= 0
  483. EM.stop
  484. end
  485. end
  486. end
  487. it "should handle Mysql error in varz" do
  488. pending "This test is not capatiable with mysql2 conenction pool."
  489. EM.run do
  490. node = VCAP::Services::Mysql::Node.new(@opts)
  491. EM.add_timer(1) do
  492. # drop mysql connection
  493. node.pool.close
  494. varz = nil
  495. expect {varz = node.varz_details}.should_not raise_error
  496. varz.should == {}
  497. EM.stop
  498. end
  499. end
  500. end
  501. it "should provide provision/binding served info in varz" do
  502. EM.run do
  503. v1 = @node.varz_details
  504. db = @node.provision(@default_plan)
  505. binding = @node.bind(db["name"], [])
  506. @test_dbs[db] = [binding]
  507. v2 = @node.varz_details
  508. (v2[:provision_served] - v1[:provision_served]).should == 1
  509. (v2[:binding_served] - v1[:binding_served]).should == 1
  510. EM.stop
  511. end
  512. end
  513. it "should report instance disk size in varz" do
  514. EM.run do
  515. v = @node.varz_details
  516. instance = v[:database_status].find {|d| d[:name] == @db["name"]}
  517. instance.should_not be_nil
  518. instance[:size].should >= 0
  519. EM.stop
  520. end
  521. end
  522. it "should report node status in healthz" do
  523. pending "This test is not capatiable with mysql2 conenction pool."
  524. EM.run do
  525. healthz = @node.healthz_details()
  526. healthz[:self].should == "ok"
  527. node = VCAP::Services::Mysql::Node.new(@opts)
  528. EM.add_timer(1) do
  529. node.pool.close
  530. healthz = node.healthz_details()
  531. healthz[:self].should == "fail"
  532. EM.stop
  533. end
  534. end
  535. end
  536. it "should report correct health status when user modify instance password" do
  537. EM.run do
  538. conn = connect_to_mysql(@db)
  539. a = conn.query("set password for #{@db['user']}@'localhost' = PASSWORD('newpass')")
  540. healthz = @node.healthz_details()
  541. healthz[:self].should == "ok"
  542. healthz[@db['name'].to_sym].should == "password-modified"
  543. EM.stop
  544. end
  545. end
  546. it "should close extra mysql connections after generate healthz" do
  547. EM.run do
  548. @node.pool.with_connection do |connection|
  549. res = connection.query("show processlist")
  550. conns_before_healthz = res.count
  551. healthz = @node.healthz_details()
  552. healthz.keys.size.should >= 2
  553. res = connection.query("show processlist")
  554. conns_after_healthz = res.count
  555. conns_before_healthz.should == conns_after_healthz
  556. end
  557. EM.stop
  558. end
  559. end
  560. it "should report instance status in healthz" do
  561. EM.run do
  562. healthz = @node.healthz_details()
  563. instance = @db['name']
  564. healthz[instance.to_sym].should == "ok"
  565. @node.pool.with_connection do |connection|
  566. connection.query("Drop database #{instance}")
  567. healthz = @node.healthz_details()
  568. healthz[instance.to_sym].should == "fail"
  569. # restore db so cleanup code doesn't complain.
  570. connection.query("create database #{instance}")
  571. end
  572. EM.stop
  573. end
  574. end
  575. it "should be thread safe" do
  576. EM.run do
  577. provision_served = @node.provision_served
  578. binding_served = @node.binding_served
  579. NUM = 20
  580. threads = []
  581. NUM.times do
  582. threads << Thread.new do
  583. db = @node.provision(@default_plan)
  584. binding = @node.bind(db["name"], @default_opts)
  585. @node.unprovision(db["name"], [binding])
  586. end
  587. end
  588. threads.each {|t| t.join}
  589. provision_served.should == @node.provision_served - NUM
  590. binding_served.should == @node.binding_served - NUM
  591. EM.stop
  592. end
  593. end
  594. it "should enforce max connection limitation per user account" do
  595. EM.run do
  596. opts = @opts.dup
  597. opts[:max_user_conns] = 1 # easy for testing
  598. node = VCAP::Services::Mysql::Node.new(opts)
  599. EM.add_timer(1) do
  600. db = node.provision(@default_plan)
  601. binding = node.bind(db["name"], @default_opts)
  602. @test_dbs[db] = [binding]
  603. expect { conn = connect_to_mysql(db) }.should_not raise_error
  604. expect { conn = connect_to_mysql(binding) }.should_not raise_error
  605. EM.stop
  606. end
  607. end
  608. end
  609. after:each do
  610. @test_dbs.keys.each do |db|
  611. begin
  612. name = db["name"]
  613. @node.unprovision(name, @test_dbs[db])
  614. @node.logger.info("Clean up temp database: #{name}")
  615. rescue => e
  616. @node.logger.info("Error during cleanup #{e}")
  617. end
  618. end if @test_dbs
  619. end
  620. end