Parent

Included Modules

Class Index [+]

Quicksearch

TaskJuggler::ProjectServer

The ProjectServer objects are created from the ProjectBroker to handle the data of a particular project. Each ProjectServer runs in a separate process that is forked-off in the constructor. Any action such as adding more files or generating a report will cause the process to fork again, creating a ReportServer object. This way the initially loaded project can be modified but the original version is always preserved for subsequent calls. Each ProjectServer process has a unique secret authentication key that only the ProjectBroker knows. It will pass it with the URI of the ProjectServer to the client to permit direct access to the ProjectServer.

Attributes

authKey[R]
uri[R]

Public Class Methods

new(daemonAuthKey, projectData = nil, logConsole = false) click to toggle source
     # File lib/taskjuggler/daemon/ProjectServer.rb, line 41
 41:     def initialize(daemonAuthKey, projectData = nil, logConsole = false)
 42:       @daemonAuthKey = daemonAuthKey
 43:       @projectData = projectData
 44:       # Since we are still in the ProjectBroker process, the current DRb
 45:       # server is still the ProjectBroker DRb server.
 46:       @daemonURI = DRb.current_server.uri
 47:       # Used later to store the DRbObject of the ProjectBroker.
 48:       @daemon = nil
 49:       initIntercom
 50: 
 51:       @logConsole = logConsole
 52:       @pid = nil
 53:       @uri = nil
 54: 
 55:       # A reference to the TaskJuggler object that holds the project data.
 56:       @tj = nil
 57:       # The current state of the project.
 58:       @state = :new
 59:       # A time stamp when the last @state update happened.
 60:       @stateUpdated = TjTime.new
 61:       # A lock to protect access to @state
 62:       @stateLock = Monitor.new
 63: 
 64:       # A Queue to asynchronously generate new ReportServer objects.
 65:       @reportServerRequests = Queue.new
 66: 
 67:       # A list of active ReportServer objects
 68:       @reportServers = []
 69:       @reportServers.extend(MonitorMixin)
 70: 
 71:       @lastPing = TjTime.new
 72: 
 73:       # We've started a DRb server before. This will continue to live somewhat
 74:       # in the child. All attempts to create a DRb connection from the child
 75:       # to the parent will end up in the child again. So we use a Pipe to
 76:       # communicate the URI of the child DRb server to the parent. The
 77:       # communication from the parent to the child is not affected by the
 78:       # zombie DRb server in the child process.
 79:       rd, wr = IO.pipe
 80: 
 81:       if (@pid = fork) == 1
 82:         @log.fatal('ProjectServer fork failed')
 83:       elsif @pid.nil?
 84:         # This is the child
 85:         if @logConsole
 86:           # If the Broker wasn't daemonized, log stdout and stderr to PID
 87:           # specific files.
 88:           $stderr.reopen("tj3d.ps.#{$$}.stderr", 'w')
 89:           $stdout.reopen("tj3d.ps.#{$$}.stdout", 'w')
 90:         end
 91:         begin
 92:           $SAFE = 1
 93:           DRb.install_acl(ACL.new(] deny all allow 127.0.0.1 ]))
 94:           DRb.start_service
 95:           iFace = ProjectServerIface.new(self)
 96:           begin
 97:             @uri = DRb.start_service('druby://127.0.0.1:0', iFace).uri
 98:             @log.debug("Project server is listening on #{@uri}")
 99:           rescue
100:             @log.fatal("ProjectServer can't start DRb: #{$!}")
101:           end
102: 
103:           # Send the URI of the newly started DRb server to the parent process.
104:           rd.close
105:           wr.write @uri
106:           wr.close
107: 
108:           # Start a Thread that waits for the @terminate flag to be set and does
109:           # other background tasks.
110:           startTerminator
111:           # Start another Thread that will be used to fork-off ReportServer
112:           # processes.
113:           startHousekeeping
114: 
115:           # Cleanup the DRb threads
116:           DRb.thread.join
117:           @log.debug('Project server terminated')
118:           exit 0
119:         rescue
120:           $stderr.print $!.to_s
121:           $stderr.print $!.backtrace.join("\n")
122:           @log.fatal("ProjectServer can't start DRb: #{$!}")
123:         end
124:       else
125:         # This is the parent
126:         Process.detach(@pid)
127:         wr.close
128:         @uri = rd.read
129:         rd.close
130:       end
131:     end

Public Instance Methods

getProjectName() click to toggle source

Return the name of the loaded project or nil.

     # File lib/taskjuggler/daemon/ProjectServer.rb, line 174
174:     def getProjectName
175:       return nil unless @tj
176:       restartTimer
177:       @tj.projectName
178:     end
getReportList() click to toggle source

Return a list of the HTML reports defined for the project.

     # File lib/taskjuggler/daemon/ProjectServer.rb, line 181
181:     def getReportList
182:       return [] unless @tj && (project = @tj.project)
183:       list = []
184:       project.reports.each do |report|
185:         unless report.get('formats').empty?
186:           list << [ report.fullId, report.name ]
187:         end
188:       end
189:       restartTimer
190:       list
191:     end
getReportServer() click to toggle source

This function triggers the creation of a new ReportServer process. It will return the URI and the authentication key of this new server.

     # File lib/taskjuggler/daemon/ProjectServer.rb, line 195
195:     def getReportServer
196:       # ReportServer objects only make sense for successfully scheduled
197:       # projects.
198:       return [ nil, nil ] unless @state == :ready
199: 
200:       # The ReportServer will be created asynchronously in another Thread. To
201:       # find it in the @reportServers list, we create a unique tag to identify
202:       # it.
203:       tag = rand(99999999999999)
204:       @log.debug("Pushing #{tag} onto report server request queue")
205:       @reportServerRequests.push(tag)
206: 
207:       # Now wait until the new ReportServer shows up in the list.
208:       reportServer = nil
209:       while reportServer.nil?
210:         @reportServers.synchronize do
211:           @reportServers.each do |rs|
212:             reportServer = rs if rs.tag == tag
213:           end
214:         end
215:         # It should not take that long, so we use a short idle time here.
216:         sleep 0.1 if reportServer.nil?
217:       end
218: 
219:       @log.debug("Got report server with URI #{reportServer.uri} for " +
220:                  "tag #{tag}")
221:       restartTimer
222:       [ reportServer.uri, reportServer.authKey ]
223:     end
loadProject(args) click to toggle source

Wait until the project load has been finished. The result is true if the project scheduled without errors. Otherwise the result is false. args is an Array of Strings. The first element is the working directory. The second one is the master project file (.tjp file). Additionally a list of optional .tji files can be provided.

     # File lib/taskjuggler/daemon/ProjectServer.rb, line 138
138:     def loadProject(args)
139:       dirAndFiles = args.dup.untaint
140:       # The first argument is the working directory
141:       Dir.chdir(args.shift.untaint)
142: 
143:       # Save a time stamp of when the project file loading started.
144:       @modifiedCheck = TjTime.new
145: 
146:       updateState(:loading, dirAndFiles, false)
147:       @tj = TaskJuggler.new(true)
148: 
149:       # Parse all project files
150:       unless @tj.parse(args, true)
151:         @log.error("Parsing of #{args.join(' ')} failed")
152:         updateState(:failed, nil, false)
153:         @terminate = true
154:         return false
155:       end
156: 
157:       # Then schedule the project
158:       unless @tj.schedule
159:         @log.error("Scheduling of project #{@tj.projectId} failed")
160:         updateState(:failed, @tj.projectId, false)
161:         Log.exit('scheduler')
162:         @terminate = true
163:         return false
164:       end
165: 
166:       # Great, everything went fine. We've got a project to work with.
167:       updateState(:ready, @tj.projectId, false)
168:       @log.info("Project #{@tj.projectId} loaded")
169:       restartTimer
170:       true
171:     end
ping() click to toggle source

This function is called regularly by the ProjectBroker process to check that the ProjectServer is still operating properly.

     # File lib/taskjuggler/daemon/ProjectServer.rb, line 227
227:     def ping
228:       # Store the time stamp. If we don't get the ping for some time, we
229:       # assume the ProjectBroker has died.
230:       @lastPing = TjTime.new
231: 
232:       # Now also check our ReportServers if they are still there. If not, we
233:       # can remove them from the @reportServers list.
234:       @reportServers.synchronize do
235:         deadServers = []
236:         @reportServers.each do |rs|
237:           unless rs.ping
238:             deadServers << rs
239:           end
240:         end
241:         @reportServers.delete_if { |rs| deadServers.include?(rs) }
242:       end
243:     end

Private Instance Methods

startHousekeeping() click to toggle source
     # File lib/taskjuggler/daemon/ProjectServer.rb, line 264
264:     def startHousekeeping
265:       Thread.new do
266:         begin
267:           loop do
268:             # Was the project data provided during object creation?
269:             # Then we load the data here.
270:             if @projectData
271:               loadProject(@projectData)
272:               @projectData = nil
273:             end
274: 
275:             # Check every 60 seconds if the input files have been modified.
276:             # Don't check if we already know it has been modified.
277:             if @stateLock.synchronize { @state == :ready && !@modified &&
278:                                         @modifiedCheck + 60 < TjTime.new }
279:               # Reset the timer
280:               @stateLock.synchronize { @modifiedCheck = TjTime.new }
281: 
282:               if @tj.project.inputFiles.modified?
283:                 @log.info("Project #{@tj.projectId} has been modified")
284:                 updateState(:ready, @tj.projectId, true)
285:               end
286:             end
287: 
288:             # Check for pending requests for new ReportServers.
289:             unless @reportServerRequests.empty?
290:               tag = @reportServerRequests.pop
291:               @log.debug("Popped #{tag}")
292:               # Create an new entry for the @reportServers list.
293:               rsr = ReportServerRecord.new(tag)
294:               @log.debug("RSR created")
295:               # Create a new ReportServer object that runs as a separate
296:               # process. The constructor will tell us the URI and authentication
297:               # key of the new ReportServer.
298:               rs = ReportServer.new(@tj, @logConsole)
299:               rsr.uri = rs.uri
300:               rsr.authKey = rs.authKey
301:               @log.debug("Adding ReportServer with URI #{rsr.uri} to list")
302:               # Add the new ReportServer to our list.
303:               @reportServers.synchronize do
304:                 @reportServers << rsr
305:               end
306:             end
307: 
308:             # Some state changing operations are not atomic. Since the client
309:             # can die during the transaction, the server might hang in some
310:             # states. Here we define timeout for each state. If the timeout is
311:             # not 0 and exceeded, we immediately terminate the process.
312:             timeouts = { :new => 10, :loading => 15 * 60, :failed => 60,
313:                          :ready => 0 }
314:             if timeouts[@state] > 0 &&
315:                TjTime.new - @stateUpdated > timeouts[@state]
316:               @log.fatal("Reached timeout for state #{@state}. Terminating.")
317:             end
318: 
319:             # If we have not received a ping from the ProjectBroker for 2
320:             # minutes, we assume it has died and terminate as well.
321:             if TjTime.new - @lastPing > 180
322:               @log.fatal('Heartbeat from daemon lost. Terminating.')
323:             end
324:             sleep 1
325:           end
326:         rescue
327:           # Make sure we get a backtrace for this thread.
328:           $stderr.print $!.to_s
329:           $stderr.print $!.backtrace.join("\n")
330:           @log.fatal("ProjectServer housekeeping error: #{$!}")
331:         end
332:       end
333:     end
updateState(state, filesOrId, modified) click to toggle source

Update the state, id and modified state of the project locally and remotely.

     # File lib/taskjuggler/daemon/ProjectServer.rb, line 249
249:     def updateState(state, filesOrId, modified)
250:       begin
251:         @daemon = DRbObject.new(nil, @daemonURI) unless @daemon
252:         @daemon.updateState(@daemonAuthKey, @authKey, filesOrId, state, modified)
253:       rescue
254:         @log.fatal("Can't update state with daemon: #{$!}")
255:       end
256:       @stateLock.synchronize do
257:         @state = state
258:         @stateUpdated = TjTime.new
259:         @modified = modified
260:         @modifiedCheck = TjTime.new
261:       end
262:     end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.