Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(282)

Side by Side Diff: third_party/buildbot_7_12/buildbot/test/test_locks.py

Issue 12207158: Bye bye buildbot 0.7.12. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 # -*- test-case-name: buildbot.test.test_locks -*-
2
3 import random
4
5 from twisted.trial import unittest
6 from twisted.internet import defer, reactor
7
8 from buildbot import master
9 from buildbot.steps import dummy
10 from buildbot.sourcestamp import SourceStamp
11 from buildbot.process.base import BuildRequest
12 from buildbot.test.runutils import RunMixin
13 from buildbot import locks
14
15 def claimHarder(lock, owner, la):
16 """Return a Deferred that will fire when the lock is claimed. Keep trying
17 until we succeed."""
18 if lock.isAvailable(la):
19 #print "claimHarder(%s): claiming" % owner
20 lock.claim(owner, la)
21 return defer.succeed(lock)
22 #print "claimHarder(%s): waiting" % owner
23 d = lock.waitUntilMaybeAvailable(owner, la)
24 d.addCallback(claimHarder, owner, la)
25 return d
26
27 def hold(lock, owner, la, mode="now"):
28 if mode == "now":
29 lock.release(owner, la)
30 elif mode == "very soon":
31 reactor.callLater(0, lock.release, owner, la)
32 elif mode == "soon":
33 reactor.callLater(0.1, lock.release, owner, la)
34
35 class Unit(unittest.TestCase):
36 def testNowCounting(self):
37 lid = locks.MasterLock('dummy')
38 la = locks.LockAccess(lid, 'counting')
39 return self._testNow(la)
40
41 def testNowExclusive(self):
42 lid = locks.MasterLock('dummy')
43 la = locks.LockAccess(lid, 'exclusive')
44 return self._testNow(la)
45
46 def _testNow(self, la):
47 l = locks.BaseLock("name")
48 self.failUnless(l.isAvailable(la))
49 l.claim("owner1", la)
50 self.failIf(l.isAvailable(la))
51 l.release("owner1", la)
52 self.failUnless(l.isAvailable(la))
53
54 def testNowMixed1(self):
55 """ Test exclusive is not possible when a counting has the lock """
56 lid = locks.MasterLock('dummy')
57 lac = locks.LockAccess(lid, 'counting')
58 lae = locks.LockAccess(lid, 'exclusive')
59 l = locks.BaseLock("name", maxCount=2)
60 self.failUnless(l.isAvailable(lac))
61 l.claim("count-owner", lac)
62 self.failIf(l.isAvailable(lae))
63 l.release("count-owner", lac)
64 self.failUnless(l.isAvailable(lac))
65
66 def testNowMixed2(self):
67 """ Test counting is not possible when an exclsuive has the lock """
68 lid = locks.MasterLock('dummy')
69 lac = locks.LockAccess(lid, 'counting')
70 lae = locks.LockAccess(lid, 'exclusive')
71 l = locks.BaseLock("name", maxCount=2)
72 self.failUnless(l.isAvailable(lae))
73 l.claim("count-owner", lae)
74 self.failIf(l.isAvailable(lac))
75 l.release("count-owner", lae)
76 self.failUnless(l.isAvailable(lae))
77
78 def testLaterCounting(self):
79 lid = locks.MasterLock('dummy')
80 la = locks.LockAccess(lid, 'counting')
81 return self._testLater(la)
82
83 def testLaterExclusive(self):
84 lid = locks.MasterLock('dummy')
85 la = locks.LockAccess(lid, 'exclusive')
86 return self._testLater(la)
87
88 def _testLater(self, la):
89 lock = locks.BaseLock("name")
90 d = claimHarder(lock, "owner1", la)
91 d.addCallback(lambda lock: lock.release("owner1", la))
92 return d
93
94 def testCompetitionCounting(self):
95 lid = locks.MasterLock('dummy')
96 la = locks.LockAccess(lid, 'counting')
97 return self._testCompetition(la)
98
99 def testCompetitionExclusive(self):
100 lid = locks.MasterLock('dummy')
101 la = locks.LockAccess(lid, 'exclusive')
102 return self._testCompetition(la)
103
104 def _testCompetition(self, la):
105 lock = locks.BaseLock("name")
106 d = claimHarder(lock, "owner1", la)
107 d.addCallback(self._claim1, la)
108 return d
109 def _claim1(self, lock, la):
110 # we should have claimed it by now
111 self.failIf(lock.isAvailable(la))
112 # now set up two competing owners. We don't know which will get the
113 # lock first.
114 d2 = claimHarder(lock, "owner2", la)
115 d2.addCallback(hold, "owner2", la, "now")
116 d3 = claimHarder(lock, "owner3", la)
117 d3.addCallback(hold, "owner3", la, "soon")
118 dl = defer.DeferredList([d2,d3])
119 dl.addCallback(self._cleanup, lock, la)
120 # and release the lock in a moment
121 reactor.callLater(0.1, lock.release, "owner1", la)
122 return dl
123
124 def _cleanup(self, res, lock, la):
125 d = claimHarder(lock, "cleanup", la)
126 d.addCallback(lambda lock: lock.release("cleanup", la))
127 return d
128
129 def testRandomCounting(self):
130 lid = locks.MasterLock('dummy')
131 la = locks.LockAccess(lid, 'counting')
132 return self._testRandom(la)
133
134 def testRandomExclusive(self):
135 lid = locks.MasterLock('dummy')
136 la = locks.LockAccess(lid, 'exclusive')
137 return self._testRandom(la)
138
139 def _testRandom(self, la):
140 lock = locks.BaseLock("name")
141 dl = []
142 for i in range(100):
143 owner = "owner%d" % i
144 mode = random.choice(["now", "very soon", "soon"])
145 d = claimHarder(lock, owner, la)
146 d.addCallback(hold, owner, la, mode)
147 dl.append(d)
148 d = defer.DeferredList(dl)
149 d.addCallback(self._cleanup, lock, la)
150 return d
151
152 class Multi(unittest.TestCase):
153 def testNowCounting(self):
154 lid = locks.MasterLock('dummy')
155 la = locks.LockAccess(lid, 'counting')
156 lock = locks.BaseLock("name", 2)
157 self.failUnless(lock.isAvailable(la))
158 lock.claim("owner1", la)
159 self.failUnless(lock.isAvailable(la))
160 lock.claim("owner2", la)
161 self.failIf(lock.isAvailable(la))
162 lock.release("owner1", la)
163 self.failUnless(lock.isAvailable(la))
164 lock.release("owner2", la)
165 self.failUnless(lock.isAvailable(la))
166
167 def testLaterCounting(self):
168 lid = locks.MasterLock('dummy')
169 la = locks.LockAccess(lid, 'counting')
170 lock = locks.BaseLock("name", 2)
171 lock.claim("owner1", la)
172 lock.claim("owner2", la)
173 d = claimHarder(lock, "owner3", la)
174 d.addCallback(lambda lock: lock.release("owner3", la))
175 lock.release("owner2", la)
176 lock.release("owner1", la)
177 return d
178
179 def _cleanup(self, res, lock, count, la):
180 dl = []
181 for i in range(count):
182 d = claimHarder(lock, "cleanup%d" % i, la)
183 dl.append(d)
184 d2 = defer.DeferredList(dl)
185 # once all locks are claimed, we know that any previous owners have
186 # been flushed out
187 def _release(res):
188 for i in range(count):
189 lock.release("cleanup%d" % i, la)
190 d2.addCallback(_release)
191 return d2
192
193 def testRandomCounting(self):
194 lid = locks.MasterLock('dummy')
195 la = locks.LockAccess(lid, 'counting')
196 COUNT = 5
197 lock = locks.BaseLock("name", COUNT)
198 dl = []
199 for i in range(100):
200 owner = "owner%d" % i
201 mode = random.choice(["now", "very soon", "soon"])
202 d = claimHarder(lock, owner, la)
203 def _check(lock):
204 self.failIf(len(lock.owners) > COUNT)
205 return lock
206 d.addCallback(_check)
207 d.addCallback(hold, owner, la, mode)
208 dl.append(d)
209 d = defer.DeferredList(dl)
210 d.addCallback(self._cleanup, lock, COUNT, la)
211 return d
212
213 class Dummy:
214 pass
215
216 def slave(slavename):
217 slavebuilder = Dummy()
218 slavebuilder.slave = Dummy()
219 slavebuilder.slave.slavename = slavename
220 return slavebuilder
221
222 class MakeRealLock(unittest.TestCase):
223
224 def make(self, lockid):
225 return lockid.lockClass(lockid)
226
227 def testMaster(self):
228 mid1 = locks.MasterLock("name1")
229 mid2 = locks.MasterLock("name1")
230 mid3 = locks.MasterLock("name3")
231 mid4 = locks.MasterLock("name1", 3)
232 self.failUnlessEqual(mid1, mid2)
233 self.failIfEqual(mid1, mid3)
234 # they should all be hashable
235 d = {mid1: 1, mid2: 2, mid3: 3, mid4: 4}
236
237 l1 = self.make(mid1)
238 self.failUnlessEqual(l1.name, "name1")
239 self.failUnlessEqual(l1.maxCount, 1)
240 self.failUnlessIdentical(l1.getLock(slave("slave1")), l1)
241 l4 = self.make(mid4)
242 self.failUnlessEqual(l4.name, "name1")
243 self.failUnlessEqual(l4.maxCount, 3)
244 self.failUnlessIdentical(l4.getLock(slave("slave1")), l4)
245
246 def testSlave(self):
247 sid1 = locks.SlaveLock("name1")
248 sid2 = locks.SlaveLock("name1")
249 sid3 = locks.SlaveLock("name3")
250 sid4 = locks.SlaveLock("name1", maxCount=3)
251 mcfs = {"bigslave": 4, "smallslave": 1}
252 sid5 = locks.SlaveLock("name1", maxCount=3, maxCountForSlave=mcfs)
253 mcfs2 = {"bigslave": 4, "smallslave": 1}
254 sid5a = locks.SlaveLock("name1", maxCount=3, maxCountForSlave=mcfs2)
255 mcfs3 = {"bigslave": 1, "smallslave": 99}
256 sid5b = locks.SlaveLock("name1", maxCount=3, maxCountForSlave=mcfs3)
257 self.failUnlessEqual(sid1, sid2)
258 self.failIfEqual(sid1, sid3)
259 self.failIfEqual(sid1, sid4)
260 self.failIfEqual(sid1, sid5)
261 self.failUnlessEqual(sid5, sid5a)
262 self.failIfEqual(sid5a, sid5b)
263 # they should all be hashable
264 d = {sid1: 1, sid2: 2, sid3: 3, sid4: 4, sid5: 5, sid5a: 6, sid5b: 7}
265
266 l1 = self.make(sid1)
267 self.failUnlessEqual(l1.name, "name1")
268 self.failUnlessEqual(l1.maxCount, 1)
269 l1s1 = l1.getLock(slave("slave1"))
270 self.failIfIdentical(l1s1, l1)
271
272 l4 = self.make(sid4)
273 self.failUnlessEqual(l4.maxCount, 3)
274 l4s1 = l4.getLock(slave("slave1"))
275 self.failUnlessEqual(l4s1.maxCount, 3)
276
277 l5 = self.make(sid5)
278 l5s1 = l5.getLock(slave("bigslave"))
279 l5s2 = l5.getLock(slave("smallslave"))
280 l5s3 = l5.getLock(slave("unnamedslave"))
281 self.failUnlessEqual(l5s1.maxCount, 4)
282 self.failUnlessEqual(l5s2.maxCount, 1)
283 self.failUnlessEqual(l5s3.maxCount, 3)
284
285 class GetLock(unittest.TestCase):
286 def testGet(self):
287 # the master.cfg file contains "lock ids", which are instances of
288 # MasterLock and SlaveLock but which are not actually Locks per se.
289 # When the build starts, these markers are turned into RealMasterLock
290 # and RealSlaveLock instances. This insures that any builds running
291 # on slaves that were unaffected by the config change are still
292 # referring to the same Lock instance as new builds by builders that
293 # *were* affected by the change. There have been bugs in the past in
294 # which this didn't happen, and the Locks were bypassed because half
295 # the builders were using one incarnation of the lock while the other
296 # half were using a separate (but equal) incarnation.
297 #
298 # Changing the lock id in any way should cause it to be replaced in
299 # the BotMaster. This will result in a couple of funky artifacts:
300 # builds in progress might pay attention to a different lock, so we
301 # might bypass the locking for the duration of a couple builds.
302 # There's also the problem of old Locks lingering around in
303 # BotMaster.locks, but they're small and shouldn't really cause a
304 # problem.
305
306 b = master.BotMaster()
307 l1 = locks.MasterLock("one")
308 l1a = locks.MasterLock("one")
309 l2 = locks.MasterLock("one", maxCount=4)
310
311 rl1 = b.getLockByID(l1)
312 rl2 = b.getLockByID(l1a)
313 self.failUnlessIdentical(rl1, rl2)
314 rl3 = b.getLockByID(l2)
315 self.failIfIdentical(rl1, rl3)
316
317 s1 = locks.SlaveLock("one")
318 s1a = locks.SlaveLock("one")
319 s2 = locks.SlaveLock("one", maxCount=4)
320 s3 = locks.SlaveLock("one", maxCount=4,
321 maxCountForSlave={"a":1, "b":2})
322 s3a = locks.SlaveLock("one", maxCount=4,
323 maxCountForSlave={"a":1, "b":2})
324 s4 = locks.SlaveLock("one", maxCount=4,
325 maxCountForSlave={"a":4, "b":4})
326
327 rl1 = b.getLockByID(s1)
328 rl2 = b.getLockByID(s1a)
329 self.failUnlessIdentical(rl1, rl2)
330 rl3 = b.getLockByID(s2)
331 self.failIfIdentical(rl1, rl3)
332 rl4 = b.getLockByID(s3)
333 self.failIfIdentical(rl1, rl4)
334 self.failIfIdentical(rl3, rl4)
335 rl5 = b.getLockByID(s3a)
336 self.failUnlessIdentical(rl4, rl5)
337 rl6 = b.getLockByID(s4)
338 self.failIfIdentical(rl5, rl6)
339
340
341
342 class LockStep(dummy.Dummy):
343 def start(self):
344 number = self.build.requests[0].number
345 self.build.requests[0].events.append(("start", number))
346 dummy.Dummy.start(self)
347 def done(self):
348 number = self.build.requests[0].number
349 self.build.requests[0].events.append(("done", number))
350 dummy.Dummy.done(self)
351
352 config_1 = """
353 from buildbot import locks
354 from buildbot.process import factory
355 from buildbot.buildslave import BuildSlave
356 from buildbot.config import BuilderConfig
357 s = factory.s
358 from buildbot.test.test_locks import LockStep
359
360 BuildmasterConfig = c = {}
361 c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')]
362 c['schedulers'] = []
363 c['slavePortnum'] = 0
364
365 first_lock = locks.SlaveLock('first')
366 second_lock = locks.MasterLock('second')
367 f1 = factory.BuildFactory([s(LockStep, timeout=2, locks=[first_lock])])
368 f2 = factory.BuildFactory([s(LockStep, timeout=3, locks=[second_lock])])
369 f3 = factory.BuildFactory([s(LockStep, timeout=2, locks=[])])
370
371 b1a = BuilderConfig(name='full1a', slavename='bot1', factory=f1)
372 b1b = BuilderConfig(name='full1b', slavename='bot1', factory=f1)
373 b1c = BuilderConfig(name='full1c', slavename='bot1', factory=f3,
374 locks=[first_lock, second_lock])
375 b1d = BuilderConfig(name='full1d', slavename='bot1', factory=f2)
376
377 b2a = BuilderConfig(name='full2a', slavename='bot2', factory=f1)
378 b2b = BuilderConfig(name='full2b', slavename='bot2', factory=f3,
379 locks=[second_lock])
380 c['builders'] = [b1a, b1b, b1c, b1d, b2a, b2b]
381 """
382
383 config_1a = config_1 + \
384 """
385 b1b = BuilderConfig(name='full1b', builddir='1B', slavename='bot1', factory=f1)
386 c['builders'] = [b1a, b1b, b1c, b1d, b2a, b2b]
387 """
388
389
390 class Locks(RunMixin, unittest.TestCase):
391 def setUp(self):
392 N = 'test_builder'
393 RunMixin.setUp(self)
394 self.req1 = req1 = BuildRequest("forced build", SourceStamp(), N)
395 req1.number = 1
396 self.req2 = req2 = BuildRequest("forced build", SourceStamp(), N)
397 req2.number = 2
398 self.req3 = req3 = BuildRequest("forced build", SourceStamp(), N)
399 req3.number = 3
400 req1.events = req2.events = req3.events = self.events = []
401 d = self.master.loadConfig(config_1)
402 d.addCallback(lambda res: self.master.startService())
403 d.addCallback(lambda res: self.connectSlave(
404 ["full1a", "full1b", "full1c", "full1d"],
405 "bot1"))
406 d.addCallback(lambda res: self.connectSlave(["full2a", "full2b"], "bot2" ))
407 return d
408
409 def testLock1(self):
410 self.control.getBuilder("full1a").requestBuild(self.req1)
411 self.control.getBuilder("full1b").requestBuild(self.req2)
412 d = defer.DeferredList([self.req1.waitUntilFinished(),
413 self.req2.waitUntilFinished()])
414 d.addCallback(self._testLock1_1)
415 return d
416
417 def _testLock1_1(self, res):
418 # full1a should complete its step before full1b starts it
419 self.failUnlessEqual(self.events,
420 [("start", 1), ("done", 1),
421 ("start", 2), ("done", 2)])
422
423 def dont_testLock1a(self): ## disabled -- test itself is buggy
424 # just like testLock1, but we reload the config file first, with a
425 # change that causes full1b to be changed. This tickles a design bug
426 # in which full1a and full1b wind up with distinct Lock instances.
427 d = self.master.loadConfig(config_1a)
428 d.addCallback(self._testLock1a_1)
429 return d
430 def _testLock1a_1(self, res):
431 self.control.getBuilder("full1a").requestBuild(self.req1)
432 self.control.getBuilder("full1b").requestBuild(self.req2)
433 d = defer.DeferredList([self.req1.waitUntilFinished(),
434 self.req2.waitUntilFinished()])
435 d.addCallback(self._testLock1a_2)
436 return d
437
438 def _testLock1a_2(self, res):
439 # full1a should complete its step before full1b starts it
440 self.failUnlessEqual(self.events,
441 [("start", 1), ("done", 1),
442 ("start", 2), ("done", 2)])
443
444 def testLock2(self):
445 # two builds run on separate slaves with slave-scoped locks should
446 # not interfere
447 self.control.getBuilder("full1a").requestBuild(self.req1)
448 self.control.getBuilder("full2a").requestBuild(self.req2)
449 d = defer.DeferredList([self.req1.waitUntilFinished(),
450 self.req2.waitUntilFinished()])
451 d.addCallback(self._testLock2_1)
452 return d
453
454 def _testLock2_1(self, res):
455 # full2a should start its step before full1a finishes it. They run on
456 # different slaves, however, so they might start in either order.
457 self.failUnless(self.events[:2] == [("start", 1), ("start", 2)] or
458 self.events[:2] == [("start", 2), ("start", 1)])
459
460 def dont_testLock3(self): ## disabled -- test fails sporadically
461 # two builds run on separate slaves with master-scoped locks should
462 # not overlap
463 self.control.getBuilder("full1c").requestBuild(self.req1)
464 self.control.getBuilder("full2b").requestBuild(self.req2)
465 d = defer.DeferredList([self.req1.waitUntilFinished(),
466 self.req2.waitUntilFinished()])
467 d.addCallback(self._testLock3_1)
468 return d
469
470 def _testLock3_1(self, res):
471 # full2b should not start until after full1c finishes. The builds run
472 # on different slaves, so we can't really predict which will start
473 # first. The important thing is that they don't overlap.
474 self.failUnless(self.events == [("start", 1), ("done", 1),
475 ("start", 2), ("done", 2)]
476 or self.events == [("start", 2), ("done", 2),
477 ("start", 1), ("done", 1)]
478 )
479
480 # This test has been disabled due to flakeyness/intermittentness
481 # def testLock4(self):
482 # self.control.getBuilder("full1a").requestBuild(self.req1)
483 # self.control.getBuilder("full1c").requestBuild(self.req2)
484 # self.control.getBuilder("full1d").requestBuild(self.req3)
485 # d = defer.DeferredList([self.req1.waitUntilFinished(),
486 # self.req2.waitUntilFinished(),
487 # self.req3.waitUntilFinished()])
488 # d.addCallback(self._testLock4_1)
489 # return d
490 #
491 # def _testLock4_1(self, res):
492 # # full1a starts, then full1d starts (because they do not interfere).
493 # # Once both are done, full1c can run.
494 # self.failUnlessEqual(self.events,
495 # [("start", 1), ("start", 3),
496 # ("done", 1), ("done", 3),
497 # ("start", 2), ("done", 2)])
498
499 class BuilderLocks(RunMixin, unittest.TestCase):
500 config = """\
501 from buildbot import locks
502 from buildbot.process import factory
503 from buildbot.buildslave import BuildSlave
504 from buildbot.config import BuilderConfig
505 s = factory.s
506 from buildbot.test.test_locks import LockStep
507
508 BuildmasterConfig = c = {}
509 c['slaves'] = [BuildSlave('bot1', 'sekrit'), BuildSlave('bot2', 'sekrit')]
510 c['schedulers'] = []
511 c['slavePortnum'] = 0
512
513 master_lock = locks.MasterLock('master', maxCount=2)
514 f_excl = factory.BuildFactory([s(LockStep, timeout=0,
515 locks=[master_lock.access("exclusive")])])
516 f_count = factory.BuildFactory([s(LockStep, timeout=0,
517 locks=[master_lock])])
518
519 slaves = ['bot1', 'bot2']
520 c['builders'] = [
521 BuilderConfig(name='excl_A', slavenames=slaves, factory=f_excl),
522 BuilderConfig(name='excl_B', slavenames=slaves, factory=f_excl),
523 BuilderConfig(name='count_A', slavenames=slaves, factory=f_count),
524 BuilderConfig(name='count_B', slavenames=slaves, factory=f_count),
525 ]
526 """
527
528 def setUp(self):
529 N = 'test_builder'
530 RunMixin.setUp(self)
531 self.reqs = [BuildRequest("forced build", SourceStamp(), N)
532 for i in range(4)]
533 self.events = []
534 for i in range(4):
535 self.reqs[i].number = i
536 self.reqs[i].events = self.events
537 d = self.master.loadConfig(self.config)
538 d.addCallback(lambda res: self.master.startService())
539 d.addCallback(lambda res: self.connectSlave(
540 ["excl_A", "excl_B", "count_A", "count_B"], "bot1"))
541 d.addCallback(lambda res: self.connectSlave(
542 ["excl_A", "excl_B", "count_A", "count_B"], "bot2"))
543 return d
544
545 def testOrder(self):
546 self.control.getBuilder("excl_A").requestBuild(self.reqs[0])
547 self.control.getBuilder("excl_B").requestBuild(self.reqs[1])
548 self.control.getBuilder("count_A").requestBuild(self.reqs[2])
549 self.control.getBuilder("count_B").requestBuild(self.reqs[3])
550 d = defer.DeferredList([r.waitUntilFinished()
551 for r in self.reqs])
552 d.addCallback(self._testOrder)
553 return d
554
555 def _testOrder(self, res):
556 # excl_A and excl_B cannot overlap with any other steps.
557 self.assert_(("start", 0) in self.events)
558 self.assert_(("done", 0) in self.events)
559 self.assert_(self.events.index(("start", 0)) + 1 ==
560 self.events.index(("done", 0)))
561
562 self.assert_(("start", 1) in self.events)
563 self.assert_(("done", 1) in self.events)
564 self.assert_(self.events.index(("start", 1)) + 1 ==
565 self.events.index(("done", 1)))
566
567 # FIXME: We really want to test that count_A and count_B were
568 # overlapped, but don't have a reliable way to do this.
OLDNEW
« no previous file with comments | « third_party/buildbot_7_12/buildbot/test/test_ec2buildslave.py ('k') | third_party/buildbot_7_12/buildbot/test/test_maildir.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698