PageRenderTime 56ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/mysql/spec/mysql_node_spec.rb

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