webapp_worker 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ## Information
2
2
 
3
- Provides a way to have workers on your webapp servers, espeically useful for webapps that tend to scale up and down with X amount of servers in the load balancer. Also good way to not use another dependent resource like a job scheduler/queue. Keeps your application all packaged up nicely, no cron jobs to set, nothing else to think about setting up and nothing else to maintain.
3
+ Provides a way to have workers on your webapp servers, especially useful for webapps that tend to scale up and down with X amount of servers in the load balancer. Also good way to not use another dependent resource like a job scheduler/queue. Keeps your application all packaged up nicely, no cron jobs to set, nothing else to think about setting up and nothing else to maintain.
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,8 +14,10 @@ or use it in your Gemfile
14
14
 
15
15
  waw -e development -f jobs.yml (parses data)
16
16
  waw -e development -f jobs.yml -j (parses and shows jobs)
17
- waw -e development -f jobs.yml -n 1 (parses and shows next X number of jobs that will run)
17
+ waw -e development -f jobs.yml -n 4 (parses and shows the next N run times, by job)
18
18
  waw -e development -f jobs.yml -r (parses and starts to run)
19
+ waw -e development -f jobs.yml -d (parses and turns on debugging)
20
+ waw -e development -f jobs.yml -v (parses and turns on verbose logging)
19
21
 
20
22
  ## Using in your webapp
21
23
 
@@ -45,39 +47,51 @@ You don't have to use a jobs file, you can specify the yaml or hash in the code
45
47
 
46
48
  ## Example Output of waw
47
49
 
50
+ See if Webapp Worker can parse the jobs file and show you all the jobs it can run from the file
51
+
48
52
  $ waw -e local -f config/jobs.yml -j
49
53
  Job File: config/jobs.yml
50
54
 
51
55
  Host: localhost
52
56
  Mailto:
53
57
  Environment: development
54
- Amount of Jobs: 9
55
-
56
- Command to Run: rake job:run
57
- Next Run: [2012-01-03 22:00:00 -0700]
58
- Command to Run: rake job:run
59
- Next Run: [2012-01-03 22:02:00 -0700]
60
- Command to Run: rake job:run
61
- Next Run: [2012-01-03 21:12:00 -0700]
62
- Command to Run: rake job:run
63
- Next Run: [2012-01-03 21:14:00 -0700]
64
- Command to Run: rake job:run
65
- Next Run: [2012-01-03 21:16:00 -0700]
66
- Command to Run: rake job:run
67
- Next Run: [2012-01-03 21:18:00 -0700]
68
- Command to Run: rake job:run
69
- Next Run: [2012-01-03 21:22:00 -0700]
70
- Command to Run: rake job:run
71
- Next Run: [2012-01-03 21:24:00 -0700]
72
- Command to Run: rake job:run
73
- Next Run: [2012-01-03 21:30:00 -0700]
58
+ Amount of Jobs: 3
59
+
60
+ Job: rake job:run
61
+ Job: rake job:run
62
+ Job: rake job:run
63
+
64
+ See when the jobs are supposed to run next, next two times, next three times, etc...
65
+
66
+ $ waw -e local -f config/jobs.yml -n 4
67
+ Job: rake job:run
68
+ Next Run Time(s): [2012-01-03 22:00:00 -0700, 2012-01-03 22:05:00 -0700, 2012-01-03 22:10:00 -0700, 2012-01-03 22:15:00 -0700]
69
+ Job: rake job:run
70
+ Next Run Time(s): [2012-01-03 22:00:00 -0700, 2012-01-03 22:05:00 -0700, 2012-01-03 22:10:00 -0700, 2012-01-03 22:15:00 -0700]
71
+ Job: rake job:run
72
+
73
+ $ waw -e local -f config/jobs.yml -r (optional -d and -v for debug and verbose)
74
+ Running Jobs
75
+
76
+ ## Other Options
77
+
78
+ You may want to know what version of the gem Webapp Worker is using, so just send a USR1 signal to it
79
+
80
+ $ kill -s USR1 11682
81
+ Webapp Worker Version: 0.0.3
82
+
83
+ You may need to turn on debugging for the Webapp Worker while its running, so just send a USR2 signal to it
84
+
85
+ $ kill -s USR2 11682
86
+ Changed logger level to Debug
74
87
 
75
88
  ## Roadmap
76
89
 
77
- - Process also needs to understand when to die and to start back up. (new version being used in the web app server)
90
+ - Use the mailto attribute in application to actually do something
78
91
  - Start having the webapp worker registering to a central point or do UDP mutlicasting to find each other.
79
92
  - Once self registering is enabled, webapp_workers need to communicate effectively.
80
- - Once communication is esatablished webapp_workers need to do the scheduling for themselves.
93
+ - Once communication is established webapp_workers need to do the scheduling for themselves.
94
+ - Do logging for each type of job in a jobs directory under tmp/webapp_worker, allowing for troubleshooting.
81
95
  - Spit out reports of the different jobs and how fast they run.
82
96
 
83
97
  ## Contributing
data/bin/waw CHANGED
@@ -15,7 +15,7 @@ where [options] are:
15
15
  opt :jobfile, "A YAML config file", :type => String, :short => "-f"
16
16
  opt :run, "Run the jobs", :default => false, :short => "-r"
17
17
  opt :jobs, "Show the jobs", :default => false, :short => "-j"
18
- opt :nextrun, "Find the next possible command(s) to run (i.e. 1,2...)", :type => Integer, :short => "-n"
18
+ opt :nextrun, "Find the next N run times, by job (use an integer)", :type => Integer, :short => "-n"
19
19
  opt :debug, "Local Debug", :short => "-d"
20
20
  opt :verbose, "Verbose Output", :short => "-v"
21
21
  end
@@ -30,8 +30,8 @@ a.parse_yaml(job_file)
30
30
 
31
31
  if opts[:nextrun] != nil
32
32
  a.next_command_run?(opts[:nextrun]).each do |command,time|
33
- puts "Next Command Run: #{command}"
34
- puts " Next Run: #{time}"
33
+ puts "Job: #{command}"
34
+ puts " Next Run Time(s): #{time}"
35
35
  end
36
36
  elsif opts[:run] == false && opts[:jobs] == false
37
37
  puts
@@ -49,8 +49,7 @@ elsif opts[:run] == false && opts[:jobs] == true
49
49
  puts
50
50
  a.jobs.each do |job|
51
51
  j = WebappWorker::Job.new(job)
52
- puts "Command to Run: #{j.command}"
53
- puts " Next Run: #{j.next_run?}"
52
+ puts "Job: #{j.command}"
54
53
  end
55
54
  else
56
55
  puts "Running Jobs"
@@ -2,6 +2,7 @@ require "webapp_worker/version"
2
2
 
3
3
  require "webapp_worker/application"
4
4
  require "webapp_worker/job"
5
+ require "webapp_worker/system"
5
6
 
6
7
  module WebappWorker
7
8
  end
@@ -1,20 +1,6 @@
1
1
  require 'socket'
2
2
  require 'timeout'
3
3
  require 'open4'
4
- require 'logger'
5
-
6
- module Process
7
- class << self
8
- def alive?(pid)
9
- begin
10
- Process.kill(0, pid.to_i)
11
- true
12
- rescue Errno::ESRCH
13
- false
14
- end
15
- end
16
- end
17
- end
18
4
 
19
5
  module WebappWorker
20
6
  class Application
@@ -44,6 +30,44 @@ module WebappWorker
44
30
  return Socket.gethostname.downcase
45
31
  end
46
32
 
33
+ def check_file_modification_time
34
+ mtime = File.mtime(@file)
35
+
36
+ if mtime != @file_mtime
37
+ @file_mtime = mtime
38
+ self.parse_yaml(@file)
39
+ end
40
+ end
41
+
42
+ def next_command_run?(til)
43
+ commands = {}
44
+ c = {}
45
+ next_commands = {}
46
+ new_jobs = []
47
+
48
+ (0..til).each do |i|
49
+ @jobs.each do |j|
50
+ new_jobs << j
51
+ end
52
+ end
53
+
54
+ new_jobs.flatten.each do |job|
55
+ j = WebappWorker::Job.new(job)
56
+ commands.store(j.command,j.next_runs?(til))
57
+ end
58
+
59
+ (commands.sort_by { |key,value| value }).collect { |key,value| c.store(key,value) }
60
+
61
+ counter = 0
62
+ c.each do |key,value|
63
+ next_commands.store(key,value)
64
+ counter = counter + 1
65
+ break if counter >= @jobs.length
66
+ end
67
+
68
+ return next_commands
69
+ end
70
+
47
71
  def next_command_run_time?
48
72
  commands = {}
49
73
  c = {}
@@ -79,162 +103,26 @@ module WebappWorker
79
103
  return next_commands
80
104
  end
81
105
 
82
- def check_file_modification_time
83
- mtime = File.mtime(@file)
84
-
85
- if mtime != @file_mtime
86
- @file_mtime = mtime
87
- self.parse_yaml(@file)
88
- end
89
- end
90
-
91
- def check_for_directory
92
- dir = "/tmp/webapp_worker"
93
-
94
- if Dir.exists?(dir)
95
- else
96
- Dir.mkdir(dir, 0700)
97
- end
98
- end
99
-
100
- def create_pid(logger)
101
- logger.info "Creating Pid File at /tmp/webapp_worker/waw.pid"
102
-
103
- File.open("/tmp/webapp_worker/waw.pid", 'w') { |f| f.write(Process.pid) }
104
- $0="Web App Worker - Job File: #{@file}"
105
-
106
- logger.info "Pid File created: #{Process.pid} at /tmp/webapp_worker/waw.pid"
107
- end
108
-
109
- def check_for_process(logger)
110
- file = "/tmp/webapp_worker/waw.pid"
111
-
112
- if File.exists?(file)
113
- possible_pid = ""
114
- pid_file = File.open(file, 'r').each { |f| possible_pid+= f }
115
- pid_file.close
116
-
117
- if Process.alive?(possible_pid)
118
- puts "Already found webapp_worker running, pid is: #{possible_pid}, exiting..."
119
- logger.fatal "Found webapp_worker already running with pid: #{possible_pid}, Pid File: #{file} exiting..."
120
- exit 1
121
- else
122
- logger.warn "Found pid file, but no process running, recreating pid file with my pid: #{Process.pid}"
123
- File.delete(file)
124
- self.create_pid(logger)
125
- end
126
- else
127
- self.create_pid(logger)
128
- end
129
-
130
- logger.info "Starting Webapp Worker"
131
- end
132
-
133
- def graceful_termination(logger)
134
- stop_loop = true
135
-
136
- begin
137
- puts
138
- puts "Graceful Termination started, waiting 60 seconds before KILL signal send"
139
- logger.info "Graceful Termination started, waiting 60 seconds before KILL signal send"
140
-
141
- Timeout::timeout(60) do
142
- @command_processes.each do |pid,command|
143
- logger.debug "Sending INT Signal to #{command} Process with PID: #{pid}"
144
- begin
145
- Process.kill("INT",pid.to_i)
146
- rescue => error
147
- end
148
- end
149
-
150
- @threads.each do |thread,command|
151
- thread.join
152
- end
153
- end
154
- rescue Timeout::Error
155
- puts "Graceful Termination bypassed, killing processes and threads"
156
- logger.info "Graceful Termination bypassed, killing processes and threads"
157
-
158
- @command_processes.each do |pid,command|
159
- logger.debug "Killing #{command} Process with PID: #{pid}"
160
- begin
161
- Process.kill("KILL",pid.to_i)
162
- rescue => error
163
- end
164
- end
165
-
166
- @threads.each do |thread,command|
167
- logger.debug "Killing Command Thread: #{command}"
168
- Thread.kill(thread)
169
- end
170
- end
171
-
172
- puts "Stopping Webapp Worker"
173
- logger.info "Stopping Webapp Worker"
174
- file = "/tmp/webapp_worker/waw.pid"
175
- File.delete(file)
176
- exit 0
177
- end
178
-
179
106
  def run(debug=nil,verbose=nil)
180
- self.check_for_directory
181
-
182
- logger = Logger.new("/tmp/webapp_worker/#{@environment}.log", 5, 5242880)
183
-
184
- if debug
185
- logger.level = Logger::DEBUG
186
- elsif verbose
187
- logger.level = Logger::INFO
188
- else
189
- logger.level = Logger::WARN
190
- end
191
-
192
107
  p = Process.fork do
193
- begin
194
- self.check_for_process(logger)
195
- rescue => error
196
- puts error.inspect
197
- logger.fatal error.inspect
198
- end
199
-
200
- @command_processes = {}
201
- @threads = {}
202
- stop_loop = false
203
-
204
- Signal.trap('HUP', 'IGNORE')
108
+ #Some Setup Work
109
+ $0="WebApp Worker - Job File: #{@file}"
110
+ waw_system = WebappWorker::System.new
111
+ waw_system.setup(debug,verbose)
112
+ logger = waw_system.logger
205
113
 
206
114
  %w(INT QUIT TERM TSTP).each do |sig|
207
115
  Signal.trap(sig) do
116
+ stop_loop = true
208
117
  logger.warn "Received a #{sig} signal, stopping current commands."
209
- self.graceful_termination(logger)
118
+ waw_system.graceful_termination(@threads,@command_processes)
210
119
  end
211
120
  end
212
121
 
213
- Signal.trap('USR1') do
214
- version = WebappWorker::VERSION
215
- puts
216
- puts "Webapp Worker Version: #{version}"
217
- logger.info "Received USR1 signal, sent version: #{version}"
218
- end
219
-
220
- Signal.trap('USR2') do
221
- logger.level = Logger::DEBUG
222
- puts
223
- puts "Changed logger level to Debug"
224
- logger.info "Changed logger level to Debug"
225
- end
226
-
227
- #Signal.trap('STOP') do |s|
228
- # #Stop Looping until
229
- # stop_loop = true
230
- # logger.warn "Received signal #{s}, pausing current loop."
231
- #end
232
- #
233
- #Signal.trap('CONT') do
234
- # #Start Looping again (catch throw?)
235
- # stop_loop = false
236
- # logger.warn "Received signal #{s}, starting current loop."
237
- #end
122
+ #WebApp Worker is setup now do the real work
123
+ @command_processes = {}
124
+ @threads = {}
125
+ stop_loop = false
238
126
 
239
127
  logger.debug "Going into Loop"
240
128
  until stop_loop
@@ -263,6 +151,8 @@ module WebappWorker
263
151
  pid, stdin, stdout, stderr = Open4::popen4 command
264
152
  @command_processes.store(pid,command)
265
153
 
154
+ #make logger log to a specific job file log file
155
+
266
156
  ignored, status = Process::waitpid2 pid
267
157
 
268
158
  if status.to_i == 0
@@ -299,6 +189,5 @@ module WebappWorker
299
189
 
300
190
  Process.detach(p)
301
191
  end
302
-
303
192
  end
304
193
  end
@@ -0,0 +1,205 @@
1
+ require 'drb'
2
+ require 'etc'
3
+ require 'logger'
4
+
5
+ module WebappWorker
6
+ class System
7
+
8
+ attr_accessor :user, :tmp_dir, :pid_file, :ipc_file, :logger
9
+
10
+ def initialize(user_supplied_hash={})
11
+ standard_hash = { user:"nobody", tmp_dir:"/tmp/webapp_worker/", pid_file:"/tmp/webapp_worker/waw.pid", ipc_file:"/tmp/webapp_worker/waw", logger:"" }
12
+
13
+ user_supplied_hash = {} unless user_supplied_hash
14
+ user_supplied_hash = standard_hash.merge(user_supplied_hash)
15
+
16
+ user_supplied_hash.each do |key,value|
17
+ self.instance_variable_set("@#{key}", value)
18
+ self.class.send(:define_method, key, proc{self.instance_variable_get("@#{key}")})
19
+ self.class.send(:define_method, "#{key}=", proc{|x| self.instance_variable_set("@#{key}", x)})
20
+ end
21
+ end
22
+
23
+ def self.process_alive?(pid)
24
+ begin
25
+ Process.kill(0, pid.to_i)
26
+ true
27
+ rescue Errno::ESRCH
28
+ false
29
+ end
30
+ end
31
+
32
+ def setup(debug=nil,verbose=nil)
33
+ self.check_for_directory
34
+ self.create_logger(debug,verbose)
35
+ self.check_for_process
36
+ self.start_listening
37
+
38
+ Signal.trap('HUP', 'IGNORE')
39
+
40
+ Signal.trap('USR1') do
41
+ version = WebappWorker::VERSION
42
+ puts
43
+ puts "Webapp Worker Version: #{version}"
44
+ logger.info "Received USR1 signal, sent version: #{version}"
45
+ end
46
+
47
+ Signal.trap('USR2') do
48
+ logger.level = Logger::DEBUG
49
+ puts
50
+ puts "Changed logger level to Debug"
51
+ logger.info "Changed logger level to Debug"
52
+ end
53
+ end
54
+
55
+ def check_for_directory
56
+ if Dir.exists?(@tmp_dir)
57
+ else
58
+ Dir.mkdir(@tmp_dir, 0700)
59
+ end
60
+ end
61
+
62
+ def create_logger(debug=nil,verbose=nil)
63
+ @logger = Logger.new("#{@tmp_dir}waw.log", 5, 5242880)
64
+
65
+ if debug
66
+ @logger.level = Logger::DEBUG
67
+ elsif verbose
68
+ @logger.level = Logger::INFO
69
+ else
70
+ @logger.level = Logger::WARN
71
+ end
72
+ end
73
+
74
+ def delete_files
75
+ @logger.fatal "Deleting both PID and IPC files"
76
+
77
+ begin
78
+ File.delete(@pid_file)
79
+ @logger.fatal "Deleted PID File: #{@pid_file}"
80
+ rescue => error
81
+ @logger.fatal "Error at Deleting PID File: #{@pid_file}: #{error}"
82
+ end
83
+
84
+ begin
85
+ File.delete(@ipc_file)
86
+ @logger.fatal "Deleted IPC File: #{@ipc_file}"
87
+ rescue => error
88
+ @logger.fatal "Error at Deleting IPC File: #{@ipc_file}: #{error}"
89
+ end
90
+ end
91
+
92
+ def create_pid
93
+ self.delete_files
94
+
95
+ @logger.info "Creating Pid File at #{@pid_file}"
96
+ File.open(@pid_file, 'w') { |f| f.write(Process.pid) }
97
+ @logger.info "Pid File created: #{Process.pid} at #{@pid_file}"
98
+ end
99
+
100
+ def check_for_process
101
+ if File.exists?(@pid_file)
102
+ possible_pid = ""
103
+ pf = File.open(@pid_file, 'r').each { |f| possible_pid+= f }
104
+ pf.close
105
+
106
+ if WebappWorker::System.process_alive?(possible_pid)
107
+ version = self.check_process_version
108
+ my_version = WebappWorker::VERSION
109
+
110
+ if version.to_s == my_version.to_s
111
+ puts "Already found webapp_worker running, pid is: #{possible_pid}, exiting..."
112
+ @logger.fatal "Found webapp_worker already running with pid: #{possible_pid}, Pid File: #{@pid_file} exiting..."
113
+ exit 1
114
+ else
115
+ puts "Found old version of webapp_worker #{version}, running with pid: #{possible_pid}, asking it to terminate since my version is: #{my_version}"
116
+ @logger.fatal "Found old version of webapp_worker #{version}, running with pid: #{possible_pid}, asking it to terminate since my version is: #{my_version}"
117
+
118
+ Process.kill("INT",possible_pid.to_i)
119
+
120
+ while WebappWorker::System.process_alive?(possible_pid)
121
+ sleep 120
122
+
123
+ begin
124
+ Process.kill("KILL",possible_pid.to_i)
125
+ rescue => error
126
+ end
127
+
128
+ self.create_pid
129
+ end
130
+ end
131
+ else
132
+ @logger.warn "Found pid file, but no process running"
133
+ self.create_pid
134
+ end
135
+ else
136
+ @logger.info "Did not find a pid file"
137
+ self.create_pid
138
+ end
139
+
140
+ @logger.info "Starting Webapp Worker"
141
+ end
142
+
143
+ def start_listening
144
+ @logger.info "Starting to listen on IPC: #{@ipc_file}"
145
+
146
+ DRb.start_service("drbunix:#{@ipc_file}", WebappWorker::VERSION)
147
+
148
+ @logger.info "Now listenting on IPC: #{@ipc_file}"
149
+ end
150
+
151
+ def check_process_version
152
+ @logger.info "Asking the processes version from IPC: #{@ipc_file}"
153
+
154
+ DRb.start_service
155
+ version = DRbObject.new_with("drbunix:#{@ipc_file}", nil)
156
+
157
+ @logger.info "The process' version is: #{version}"
158
+ return version
159
+ end
160
+
161
+ def graceful_termination(threads,command_processes)
162
+ begin
163
+ puts
164
+ puts "Graceful Termination started, waiting 60 seconds before KILL signal send"
165
+ @logger.info "Graceful Termination started, waiting 60 seconds before KILL signal send"
166
+
167
+ Timeout::timeout(60) do
168
+ command_processes.each do |pid,command|
169
+ @logger.debug "Sending INT Signal to #{command} Process with PID: #{pid}"
170
+
171
+ if WebappWorker::System.process_alive?(pid)
172
+ Process.kill("INT",pid.to_i)
173
+ end
174
+ end
175
+
176
+ threads.each do |thread,command|
177
+ thread.join
178
+ end
179
+ end
180
+ rescue Timeout::Error
181
+ puts "Graceful Termination bypassed, killing processes and threads"
182
+ @logger.info "Graceful Termination bypassed, killing processes and threads"
183
+
184
+ command_processes.each do |pid,command|
185
+ @logger.debug "Killing #{command} Process with PID: #{pid}"
186
+
187
+ if WebappWorker::System.process_alive?(pid)
188
+ Process.kill("KILL",pid.to_i)
189
+ end
190
+ end
191
+
192
+ threads.each do |thread,command|
193
+ @logger.debug "Killing Command Thread: #{command}"
194
+ Thread.kill(thread)
195
+ end
196
+ end
197
+
198
+ puts "Stopping Webapp Worker"
199
+ @logger.info "Stopping Webapp Worker"
200
+
201
+ self.delete_files
202
+ exit 0
203
+ end
204
+ end
205
+ end
@@ -1,3 +1,3 @@
1
1
  module WebappWorker
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webapp_worker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-16 00:00:00.000000000Z
12
+ date: 2012-01-18 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: trollop
16
- requirement: &83276430 !ruby/object:Gem::Requirement
16
+ requirement: &80202920 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - =
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.16.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *83276430
24
+ version_requirements: *80202920
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: open4
27
- requirement: &83272260 !ruby/object:Gem::Requirement
27
+ requirement: &80201930 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - =
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: 1.3.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *83272260
35
+ version_requirements: *80201930
36
36
  description: Allow the webapp to handle your workers, no need to use a job scheduler
37
37
  email:
38
38
  - nickwillever@gmail.com
@@ -49,6 +49,7 @@ files:
49
49
  - lib/webapp_worker.rb
50
50
  - lib/webapp_worker/application.rb
51
51
  - lib/webapp_worker/job.rb
52
+ - lib/webapp_worker/system.rb
52
53
  - lib/webapp_worker/version.rb
53
54
  - webapp_worker.gemspec
54
55
  homepage: https://nictrix.github.com/webapp_worker