PageRenderTime 119ms CodeModel.GetById 45ms RepoModel.GetById 0ms app.codeStats 0ms

/tests_description.txt.rb

https://bitbucket.org/boris_pilgun/test_for_david
Ruby | 270 lines | 99 code | 63 blank | 108 comment | 5 complexity | dee98695b2c6c52b0b93360d81195187 MD5 | raw file
  1. =begin
  2. COMMON NOTES:
  3. Application is available here for public access: https://bitbucket.org/boris_pilgun/test_for_david/overview
  4. It was crated with help of composer command:
  5. "rails new myapp -m https://raw.github.com/RailsApps/rails-composer/master/composer.rb"
  6. There is not any changes on client_side part, only backend for check tests from pdf.
  7. All 3 tests are in this app.
  8. task-1:
  9. look at
  10. /db/migrate/20130624133411_create_post.rb
  11. /db/migrate/20130624135126_add_permalink_to_post.rb
  12. task-2:
  13. look at
  14. /lib/second.rb
  15. run just this separated file to check (some tests are already there; implemented by original version of Luhn algorithm author).
  16. task-3:
  17. test process described at the end of this document together with related code.
  18. =end
  19. =begin
  20. 1.
  21. Show the code you would use for a database migration that added a field permalink to an existing table called post. The migration is to also create a permalink for every existing Post record based on the title field. You can use any method you want to convert title to become permalink, the only requirements being permalink must be based on title, must be constructed of only URL valid characters, and must be unique (even if another Post has the same title and converts to the same permalink).
  22. =end
  23. #20130624135126_add_permalink_to_post.rb
  24. class AddPermalinkToPost < ActiveRecord::Migration
  25. def change
  26. add_column :post, :permalink, :string
  27. add_index :post, :permalink, :unique => true
  28. end
  29. def migrate(direction)
  30. super
  31. if direction == :up
  32. Rails.application.eager_load! #preload all code in production to access Post model
  33. Post.all.each do |post|
  34. post.update_attribute :permalink, "#{post.id} #{post.title}".parameterize
  35. puts post.permalink
  36. end
  37. puts "All permalinks updated!"
  38. end
  39. end
  40. end
  41. =begin
  42. NOTES:
  43. we have to set uniqueness pemalink for each post so we need some unique key + post.title. As key we can use post.id - it is always unique. If assume that title is blank, permalink will be same as post.id, so not blank but still unique. However we have issue here for new records, because id is available only after saving new post. This is not a problem and as solution we can use "after_create :set_permalink" callback for new records which will add permalink after object was created.
  44. Our model will looks like below in such case:
  45. =end
  46. class Post < ActiveRecord::Base
  47. attr_accessible :title
  48. set_table_name 'post'
  49. validates :title, :presence => true, :length => {:minimum => 2}
  50. #!!!!!!!!
  51. # all next code should be added after running migration with permalink field:
  52. #!!!!!!!!
  53. #validates :permalink, :presence => true, :unless => Proc.new{ new_record? }
  54. #in additional to uniqueness validation with indexes in DB we add validation to model:
  55. #validates :permalink, :uniqueness => true, :allow_nil => true
  56. #after_create :set_permalink
  57. #
  58. #private
  59. #
  60. #def set_permalink
  61. # self.update_attribute :permalink, "#{self.id} #{self.title}".parameterize
  62. #end
  63. end
  64. #=========================================================================================
  65. =begin
  66. 2.
  67. Credit cards use a check digit algorithm called the Luhn algorithm. Write a function that takes one parameter, the credit card number, and returns true or false depending on whether the number is a valid number. Similarly, write a second function that takes a number of any length and calculates the Luhn check digit and returns the original number with the check digit appended on the end.
  68. For information on the Luhn algorithm:
  69. http://en.wikipedia.org/wiki/Luhn_algorithm
  70. =end
  71. #separated method for calculating Luhn digit of source number
  72. def calculate_luhn_digit(input_num)
  73. input_num = input_num.to_s.scan(/\d/).map(&:to_i).reverse
  74. #separate values at odd & even indexes
  75. even_numbers = input_num.values_at(* input_num.each_index.select{|k| k.even?})
  76. odd_numbers = input_num.values_at(* input_num.each_index.select{|k| k.odd?})
  77. #calculate sum
  78. result = odd_numbers.reduce{|sum, new_val| sum + new_val }
  79. even_numbers.each{|num| num *= 2; result += (num > 9 ? num - 9 : num)}
  80. #additional digit
  81. result = 10 - (result % 10)
  82. result == 10 ? 0 : result
  83. end
  84. #check if source number is valid
  85. def number_is_valid?(number)
  86. number = number.to_s
  87. luhn_result = calculate_luhn_digit(number[0...number.size-1])
  88. number[number.size-1].to_i == luhn_result
  89. end
  90. #append luhn digit to the end of source number
  91. def append_luhn_digit(number)
  92. number = number.to_s
  93. "#{number}#{calculate_luhn_digit(number[0..number.size-1])}".to_i
  94. end
  95. #some tests:
  96. puts "calculate_luhn_digit method:"
  97. puts calculate_luhn_digit(7999999712)
  98. puts calculate_luhn_digit(7992739871)
  99. puts "number_is_valid? method:"
  100. puts number_is_valid?(79999997124)
  101. puts number_is_valid?(79999997129)
  102. puts number_is_valid?(79927398712)
  103. puts number_is_valid?(79927398713)
  104. puts "append_luhn_digit? method:"
  105. puts append_luhn_digit(7999999712)
  106. puts append_luhn_digit(7992739871)
  107. #=========================================================================================
  108. =begin
  109. 3.
  110. ...here is too long description...
  111. =end
  112. #>>>"Find or create an object by a unique key and update it without introducing any extra objects..."
  113. #>>>"...A line_item_id is unique in the scope of a service...."
  114. #as said above we have unique key :line_item_id in our payment model. However just for testing I'll comment out this validation (if my solution is correct then we will not have any duplicates even without validations). And now my model will looks like the below:
  115. class Payment < ActiveRecord::Base
  116. attr_accessible :balance, :line_item_id, :service_id
  117. belongs_to :service
  118. #validates :line_item_id, :uniqueness => { :scope => :service_id } #commented out for testing with parallel requests
  119. #same thing in CreatePayment migration for DB-level validation:
  120. #add_index :payments, [:line_item_id, :service_id], :unique => true
  121. validates :line_item_id, :service_id, :presence => true #validates for presence of required fields
  122. #simulates some updates in payment object with help of transaction and pessimistic DB locking
  123. def self.update_with_lock(payment, args)
  124. #using Locking::Pessimistic to prevent conflict because of update line by 2 or more threads at one time
  125. Payment.transaction do
  126. payment.lock!
  127. payment.balance = payment.balance.to_f + 10 # + args[:new_amount]
  128. payment.save!
  129. end
  130. payment
  131. end
  132. end
  133. #also we have service model:
  134. class Service < ActiveRecord::Base
  135. attr_accessible :name
  136. validates :name, :presence => true, :uniqueness => true
  137. end
  138. #to make requests we will use PaymentController#payment_process (controller#action);
  139. #SingletonPaymentWrapper instance inside of mutex sync. block has extra privilages to give us correct payment object (it will find or create record if not exists). The main idea in using singleton pattern inside mutex is to simplify getting object in single thread without any conflicts between threads which trying to access the same payment object at one time.
  140. class PaymentController < ApplicationController
  141. @@mutex = Mutex.new
  142. def payment_process
  143. payment = nil
  144. #syncronize access to SingletonPaymentWrapper class instance.
  145. @@mutex.synchronize do
  146. payment = SingletonPaymentWrapper.instance.find_or_create_payment(params)
  147. end
  148. if payment
  149. #make some updates with found/created payment object
  150. payment = Payment.update_with_lock(payment, params)
  151. render text: "#{payment.id} - #{payment.balance}"
  152. else
  153. render text: "blank"
  154. end
  155. end
  156. end
  157. #Singleton pattern implementation: SingletonPaymentWrapper class:
  158. require 'singleton'
  159. class SingletonPaymentWrapper
  160. include Singleton
  161. def initialize()
  162. end
  163. def find_or_create_payment(args)
  164. payment = nil
  165. Payment.transaction do
  166. payment = Payment.where(:line_item_id => args[:line_item_id], :service_id => args[:service_id]).first
  167. unless payment
  168. payment = Payment.create(:line_item_id => args[:line_item_id], :service_id => args[:service_id])
  169. end
  170. end
  171. payment
  172. end
  173. end
  174. =begin
  175. NOTES:
  176. 1. to make possible test all described in task 3 I've used puma web-server wich allow use multithreaded requests (max 16 at once) if enabled option in production.rb file:
  177. # Enable threaded mode
  178. config.threadsafe!
  179. 2. mutex with singleton class find||create and returns payment object in one thread and after this each request continue go to new queue iside of self.update_with_lock(payment, args) method where uses DB-level pessimistic locking for update found record safely.
  180. TEST PROCESS:
  181. from 8 consoles ($ rails c production) almost at one time manually started this script:
  182. (1..5000).map{|i| `curl \'http://localhost:3000/payment/payment_process?service_id=1&line_item_id=#{rand(100)}\'`}
  183. also all the time were monitored count of records in Payment model with this script from one more console:
  184. while(true) do
  185. sleep(5)
  186. puts Payment.count
  187. end
  188. At the end after a couple tests this solution gave me good result.
  189. =end