test_state.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. from __future__ import absolute_import, unicode_literals
  2. import pickle
  3. from decimal import Decimal
  4. from random import shuffle
  5. from time import time
  6. from itertools import count
  7. from case import Mock, patch, skip
  8. from celery import states
  9. from celery import uuid
  10. from celery.events import Event
  11. from celery.events.state import (
  12. HEARTBEAT_EXPIRE_WINDOW,
  13. HEARTBEAT_DRIFT_MAX,
  14. State,
  15. Worker,
  16. Task,
  17. heartbeat_expires,
  18. )
  19. from celery.five import range
  20. class replay(object):
  21. def __init__(self, state):
  22. self.state = state
  23. self.rewind()
  24. self.setup()
  25. self.current_clock = 0
  26. def setup(self):
  27. pass
  28. def next_event(self):
  29. ev = self.events[next(self.position)]
  30. ev['local_received'] = ev['timestamp']
  31. try:
  32. self.current_clock = ev['clock']
  33. except KeyError:
  34. ev['clock'] = self.current_clock = self.current_clock + 1
  35. return ev
  36. def __iter__(self):
  37. return self
  38. def __next__(self):
  39. try:
  40. self.state.event(self.next_event())
  41. except IndexError:
  42. raise StopIteration()
  43. next = __next__
  44. def rewind(self):
  45. self.position = count(0)
  46. return self
  47. def play(self):
  48. for _ in self:
  49. pass
  50. class ev_worker_online_offline(replay):
  51. def setup(self):
  52. self.events = [
  53. Event('worker-online', hostname='utest1'),
  54. Event('worker-offline', hostname='utest1'),
  55. ]
  56. class ev_worker_heartbeats(replay):
  57. def setup(self):
  58. self.events = [
  59. Event('worker-heartbeat', hostname='utest1',
  60. timestamp=time() - HEARTBEAT_EXPIRE_WINDOW * 2),
  61. Event('worker-heartbeat', hostname='utest1'),
  62. ]
  63. class ev_task_states(replay):
  64. def setup(self):
  65. tid = self.tid = uuid()
  66. tid2 = self.tid2 = uuid()
  67. self.events = [
  68. Event('task-received', uuid=tid, name='task1',
  69. args='(2, 2)', kwargs="{'foo': 'bar'}",
  70. retries=0, eta=None, hostname='utest1'),
  71. Event('task-started', uuid=tid, hostname='utest1'),
  72. Event('task-revoked', uuid=tid, hostname='utest1'),
  73. Event('task-retried', uuid=tid, exception="KeyError('bar')",
  74. traceback='line 2 at main', hostname='utest1'),
  75. Event('task-failed', uuid=tid, exception="KeyError('foo')",
  76. traceback='line 1 at main', hostname='utest1'),
  77. Event('task-succeeded', uuid=tid, result='4',
  78. runtime=0.1234, hostname='utest1'),
  79. Event('foo-bar'),
  80. Event('task-received', uuid=tid2, name='task2',
  81. args='(4, 4)', kwargs="{'foo': 'bar'}",
  82. retries=0, eta=None, parent_id=tid, root_id=tid,
  83. hostname='utest1'),
  84. ]
  85. def QTEV(type, uuid, hostname, clock, name=None, timestamp=None):
  86. """Quick task event."""
  87. return Event('task-{0}'.format(type), uuid=uuid, hostname=hostname,
  88. clock=clock, name=name, timestamp=timestamp or time())
  89. class ev_logical_clock_ordering(replay):
  90. def __init__(self, state, offset=0, uids=None):
  91. self.offset = offset or 0
  92. self.uids = self.setuids(uids)
  93. super(ev_logical_clock_ordering, self).__init__(state)
  94. def setuids(self, uids):
  95. uids = self.tA, self.tB, self.tC = uids or [uuid(), uuid(), uuid()]
  96. return uids
  97. def setup(self):
  98. offset = self.offset
  99. tA, tB, tC = self.uids
  100. self.events = [
  101. QTEV('received', tA, 'w1', name='tA', clock=offset + 1),
  102. QTEV('received', tB, 'w2', name='tB', clock=offset + 1),
  103. QTEV('started', tA, 'w1', name='tA', clock=offset + 3),
  104. QTEV('received', tC, 'w2', name='tC', clock=offset + 3),
  105. QTEV('started', tB, 'w2', name='tB', clock=offset + 5),
  106. QTEV('retried', tA, 'w1', name='tA', clock=offset + 7),
  107. QTEV('succeeded', tB, 'w2', name='tB', clock=offset + 9),
  108. QTEV('started', tC, 'w2', name='tC', clock=offset + 10),
  109. QTEV('received', tA, 'w3', name='tA', clock=offset + 13),
  110. QTEV('succeded', tC, 'w2', name='tC', clock=offset + 12),
  111. QTEV('started', tA, 'w3', name='tA', clock=offset + 14),
  112. QTEV('succeeded', tA, 'w3', name='TA', clock=offset + 16),
  113. ]
  114. def rewind_with_offset(self, offset, uids=None):
  115. self.offset = offset
  116. self.uids = self.setuids(uids or self.uids)
  117. self.setup()
  118. self.rewind()
  119. class ev_snapshot(replay):
  120. def setup(self):
  121. self.events = [
  122. Event('worker-online', hostname='utest1'),
  123. Event('worker-online', hostname='utest2'),
  124. Event('worker-online', hostname='utest3'),
  125. ]
  126. for i in range(20):
  127. worker = not i % 2 and 'utest2' or 'utest1'
  128. type = not i % 2 and 'task2' or 'task1'
  129. self.events.append(Event('task-received', name=type,
  130. uuid=uuid(), hostname=worker))
  131. class test_Worker:
  132. def test_equality(self):
  133. assert Worker(hostname='foo').hostname == 'foo'
  134. assert Worker(hostname='foo') == Worker(hostname='foo')
  135. assert Worker(hostname='foo') != Worker(hostname='bar')
  136. assert hash(Worker(hostname='foo')) == hash(Worker(hostname='foo'))
  137. assert hash(Worker(hostname='foo')) != hash(Worker(hostname='bar'))
  138. def test_heartbeat_expires__Decimal(self):
  139. assert heartbeat_expires(
  140. Decimal(344313.37), freq=60, expire_window=200) == 344433.37
  141. def test_compatible_with_Decimal(self):
  142. w = Worker('george@vandelay.com')
  143. timestamp, local_received = Decimal(time()), time()
  144. w.event('worker-online', timestamp, local_received, fields={
  145. 'hostname': 'george@vandelay.com',
  146. 'timestamp': timestamp,
  147. 'local_received': local_received,
  148. 'freq': Decimal(5.6335431),
  149. })
  150. assert w.alive
  151. def test_eq_ne_other(self):
  152. assert Worker('a@b.com') == Worker('a@b.com')
  153. assert Worker('a@b.com') != Worker('b@b.com')
  154. assert Worker('a@b.com') != object()
  155. def test_reduce_direct(self):
  156. w = Worker('george@vandelay.com')
  157. w.event('worker-online', 10.0, 13.0, fields={
  158. 'hostname': 'george@vandelay.com',
  159. 'timestamp': 10.0,
  160. 'local_received': 13.0,
  161. 'freq': 60,
  162. })
  163. fun, args = w.__reduce__()
  164. w2 = fun(*args)
  165. assert w2.hostname == w.hostname
  166. assert w2.pid == w.pid
  167. assert w2.freq == w.freq
  168. assert w2.heartbeats == w.heartbeats
  169. assert w2.clock == w.clock
  170. assert w2.active == w.active
  171. assert w2.processed == w.processed
  172. assert w2.loadavg == w.loadavg
  173. assert w2.sw_ident == w.sw_ident
  174. def test_update(self):
  175. w = Worker('george@vandelay.com')
  176. w.update({'idx': '301'}, foo=1, clock=30, bah='foo')
  177. assert w.idx == '301'
  178. assert w.foo == 1
  179. assert w.clock == 30
  180. assert w.bah == 'foo'
  181. def test_survives_missing_timestamp(self):
  182. worker = Worker(hostname='foo')
  183. worker.event('heartbeat')
  184. assert worker.heartbeats == []
  185. def test_repr(self):
  186. assert repr(Worker(hostname='foo'))
  187. def test_drift_warning(self):
  188. worker = Worker(hostname='foo')
  189. with patch('celery.events.state.warn') as warn:
  190. worker.event(None, time() + (HEARTBEAT_DRIFT_MAX * 2), time())
  191. warn.assert_called()
  192. assert 'Substantial drift' in warn.call_args[0][0]
  193. def test_updates_heartbeat(self):
  194. worker = Worker(hostname='foo')
  195. worker.event(None, time(), time())
  196. assert len(worker.heartbeats) == 1
  197. h1 = worker.heartbeats[0]
  198. worker.event(None, time(), time() - 10)
  199. assert len(worker.heartbeats) == 2
  200. assert worker.heartbeats[-1] == h1
  201. class test_Task:
  202. def test_equality(self):
  203. assert Task(uuid='foo').uuid == 'foo'
  204. assert Task(uuid='foo') == Task(uuid='foo')
  205. assert Task(uuid='foo') != Task(uuid='bar')
  206. assert hash(Task(uuid='foo')) == hash(Task(uuid='foo'))
  207. assert hash(Task(uuid='foo')) != hash(Task(uuid='bar'))
  208. def test_info(self):
  209. task = Task(uuid='abcdefg',
  210. name='tasks.add',
  211. args='(2, 2)',
  212. kwargs='{}',
  213. retries=2,
  214. result=42,
  215. eta=1,
  216. runtime=0.0001,
  217. expires=1,
  218. parent_id='bdefc',
  219. root_id='dedfef',
  220. foo=None,
  221. exception=1,
  222. received=time() - 10,
  223. started=time() - 8,
  224. exchange='celery',
  225. routing_key='celery',
  226. succeeded=time())
  227. assert sorted(list(task._info_fields)) == sorted(task.info().keys())
  228. assert (sorted(list(task._info_fields + ('received',))) ==
  229. sorted(task.info(extra=('received',))))
  230. assert (sorted(['args', 'kwargs']) ==
  231. sorted(task.info(['args', 'kwargs']).keys()))
  232. assert not list(task.info('foo'))
  233. def test_reduce_direct(self):
  234. task = Task(uuid='uuid', name='tasks.add', args='(2, 2)')
  235. fun, args = task.__reduce__()
  236. task2 = fun(*args)
  237. assert task == task2
  238. def test_ready(self):
  239. task = Task(uuid='abcdefg',
  240. name='tasks.add')
  241. task.event('received', time(), time())
  242. assert not task.ready
  243. task.event('succeeded', time(), time())
  244. assert task.ready
  245. def test_sent(self):
  246. task = Task(uuid='abcdefg',
  247. name='tasks.add')
  248. task.event('sent', time(), time())
  249. assert task.state == states.PENDING
  250. def test_merge(self):
  251. task = Task()
  252. task.event('failed', time(), time())
  253. task.event('started', time(), time())
  254. task.event('received', time(), time(), {
  255. 'name': 'tasks.add', 'args': (2, 2),
  256. })
  257. assert task.state == states.FAILURE
  258. assert task.name == 'tasks.add'
  259. assert task.args == (2, 2)
  260. task.event('retried', time(), time())
  261. assert task.state == states.RETRY
  262. def test_repr(self):
  263. assert repr(Task(uuid='xxx', name='tasks.add'))
  264. class test_State:
  265. def test_repr(self):
  266. assert repr(State())
  267. def test_pickleable(self):
  268. state = State()
  269. r = ev_logical_clock_ordering(state)
  270. r.play()
  271. assert pickle.loads(pickle.dumps(state))
  272. def test_task_logical_clock_ordering(self):
  273. state = State()
  274. r = ev_logical_clock_ordering(state)
  275. tA, tB, tC = r.uids
  276. r.play()
  277. now = list(state.tasks_by_time())
  278. assert now[0][0] == tA
  279. assert now[1][0] == tC
  280. assert now[2][0] == tB
  281. for _ in range(1000):
  282. shuffle(r.uids)
  283. tA, tB, tC = r.uids
  284. r.rewind_with_offset(r.current_clock + 1, r.uids)
  285. r.play()
  286. now = list(state.tasks_by_time())
  287. assert now[0][0] == tA
  288. assert now[1][0] == tC
  289. assert now[2][0] == tB
  290. @skip.todo(reason='not working')
  291. def test_task_descending_clock_ordering(self):
  292. state = State()
  293. r = ev_logical_clock_ordering(state)
  294. tA, tB, tC = r.uids
  295. r.play()
  296. now = list(state.tasks_by_time(reverse=False))
  297. assert now[0][0] == tA
  298. assert now[1][0] == tB
  299. assert now[2][0] == tC
  300. for _ in range(1000):
  301. shuffle(r.uids)
  302. tA, tB, tC = r.uids
  303. r.rewind_with_offset(r.current_clock + 1, r.uids)
  304. r.play()
  305. now = list(state.tasks_by_time(reverse=False))
  306. assert now[0][0] == tB
  307. assert now[1][0] == tC
  308. assert now[2][0] == tA
  309. def test_get_or_create_task(self):
  310. state = State()
  311. task, created = state.get_or_create_task('id1')
  312. assert task.uuid == 'id1'
  313. assert created
  314. task2, created2 = state.get_or_create_task('id1')
  315. assert task2 is task
  316. assert not created2
  317. def test_get_or_create_worker(self):
  318. state = State()
  319. worker, created = state.get_or_create_worker('george@vandelay.com')
  320. assert worker.hostname == 'george@vandelay.com'
  321. assert created
  322. worker2, created2 = state.get_or_create_worker('george@vandelay.com')
  323. assert worker2 is worker
  324. assert not created2
  325. def test_get_or_create_worker__with_defaults(self):
  326. state = State()
  327. worker, created = state.get_or_create_worker(
  328. 'george@vandelay.com', pid=30,
  329. )
  330. assert worker.hostname == 'george@vandelay.com'
  331. assert worker.pid == 30
  332. assert created
  333. worker2, created2 = state.get_or_create_worker(
  334. 'george@vandelay.com', pid=40,
  335. )
  336. assert worker2 is worker
  337. assert worker2.pid == 40
  338. assert not created2
  339. def test_worker_online_offline(self):
  340. r = ev_worker_online_offline(State())
  341. next(r)
  342. assert list(r.state.alive_workers())
  343. assert r.state.workers['utest1'].alive
  344. r.play()
  345. assert not list(r.state.alive_workers())
  346. assert not r.state.workers['utest1'].alive
  347. def test_itertasks(self):
  348. s = State()
  349. s.tasks = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}
  350. assert len(list(s.itertasks(limit=2))) == 2
  351. def test_worker_heartbeat_expire(self):
  352. r = ev_worker_heartbeats(State())
  353. next(r)
  354. assert not list(r.state.alive_workers())
  355. assert not r.state.workers['utest1'].alive
  356. r.play()
  357. assert list(r.state.alive_workers())
  358. assert r.state.workers['utest1'].alive
  359. def test_task_states(self):
  360. r = ev_task_states(State())
  361. # RECEIVED
  362. next(r)
  363. assert r.tid in r.state.tasks
  364. task = r.state.tasks[r.tid]
  365. assert task.state == states.RECEIVED
  366. assert task.received
  367. assert task.timestamp == task.received
  368. assert task.worker.hostname == 'utest1'
  369. # STARTED
  370. next(r)
  371. assert r.state.workers['utest1'].alive
  372. assert task.state == states.STARTED
  373. assert task.started
  374. assert task.timestamp == task.started
  375. assert task.worker.hostname == 'utest1'
  376. # REVOKED
  377. next(r)
  378. assert task.state == states.REVOKED
  379. assert task.revoked
  380. assert task.timestamp == task.revoked
  381. assert task.worker.hostname == 'utest1'
  382. # RETRY
  383. next(r)
  384. assert task.state == states.RETRY
  385. assert task.retried
  386. assert task.timestamp == task.retried
  387. assert task.worker.hostname, 'utest1'
  388. assert task.exception == "KeyError('bar')"
  389. assert task.traceback == 'line 2 at main'
  390. # FAILURE
  391. next(r)
  392. assert task.state == states.FAILURE
  393. assert task.failed
  394. assert task.timestamp == task.failed
  395. assert task.worker.hostname == 'utest1'
  396. assert task.exception == "KeyError('foo')"
  397. assert task.traceback == 'line 1 at main'
  398. # SUCCESS
  399. next(r)
  400. assert task.state == states.SUCCESS
  401. assert task.succeeded
  402. assert task.timestamp == task.succeeded
  403. assert task.worker.hostname == 'utest1'
  404. assert task.result == '4'
  405. assert task.runtime == 0.1234
  406. # children, parent, root
  407. r.play()
  408. assert r.tid2 in r.state.tasks
  409. task2 = r.state.tasks[r.tid2]
  410. assert task2.parent is task
  411. assert task2.root is task
  412. assert task2 in task.children
  413. def test_task_children_set_if_received_in_wrong_order(self):
  414. r = ev_task_states(State())
  415. r.events.insert(0, r.events.pop())
  416. r.play()
  417. assert r.state.tasks[r.tid2] in r.state.tasks[r.tid].children
  418. assert r.state.tasks[r.tid2].root is r.state.tasks[r.tid]
  419. assert r.state.tasks[r.tid2].parent is r.state.tasks[r.tid]
  420. def assertStateEmpty(self, state):
  421. assert not state.tasks
  422. assert not state.workers
  423. assert not state.event_count
  424. assert not state.task_count
  425. def assertState(self, state):
  426. assert state.tasks
  427. assert state.workers
  428. assert state.event_count
  429. assert state.task_count
  430. def test_freeze_while(self):
  431. s = State()
  432. r = ev_snapshot(s)
  433. r.play()
  434. def work():
  435. pass
  436. s.freeze_while(work, clear_after=True)
  437. assert not s.event_count
  438. s2 = State()
  439. r = ev_snapshot(s2)
  440. r.play()
  441. s2.freeze_while(work, clear_after=False)
  442. assert s2.event_count
  443. def test_clear_tasks(self):
  444. s = State()
  445. r = ev_snapshot(s)
  446. r.play()
  447. assert s.tasks
  448. s.clear_tasks(ready=False)
  449. assert not s.tasks
  450. def test_clear(self):
  451. r = ev_snapshot(State())
  452. r.play()
  453. assert r.state.event_count
  454. assert r.state.workers
  455. assert r.state.tasks
  456. assert r.state.task_count
  457. r.state.clear()
  458. assert not r.state.event_count
  459. assert not r.state.workers
  460. assert r.state.tasks
  461. assert not r.state.task_count
  462. r.state.clear(False)
  463. assert not r.state.tasks
  464. def test_task_types(self):
  465. r = ev_snapshot(State())
  466. r.play()
  467. assert sorted(r.state.task_types()) == ['task1', 'task2']
  468. def test_tasks_by_time(self):
  469. r = ev_snapshot(State())
  470. r.play()
  471. assert len(list(r.state.tasks_by_time())) == 20
  472. assert len(list(r.state.tasks_by_time(reverse=False))) == 20
  473. def test_tasks_by_type(self):
  474. r = ev_snapshot(State())
  475. r.play()
  476. assert len(list(r.state.tasks_by_type('task1'))) == 10
  477. assert len(list(r.state.tasks_by_type('task2'))) == 10
  478. assert len(r.state.tasks_by_type['task1']) == 10
  479. assert len(r.state.tasks_by_type['task2']) == 10
  480. def test_alive_workers(self):
  481. r = ev_snapshot(State())
  482. r.play()
  483. assert len(list(r.state.alive_workers())) == 3
  484. def test_tasks_by_worker(self):
  485. r = ev_snapshot(State())
  486. r.play()
  487. assert len(list(r.state.tasks_by_worker('utest1'))) == 10
  488. assert len(list(r.state.tasks_by_worker('utest2'))) == 10
  489. assert len(r.state.tasks_by_worker['utest1']) == 10
  490. assert len(r.state.tasks_by_worker['utest2']) == 10
  491. def test_survives_unknown_worker_event(self):
  492. s = State()
  493. s.event({
  494. 'type': 'worker-unknown-event-xxx',
  495. 'foo': 'bar',
  496. })
  497. s.event({
  498. 'type': 'worker-unknown-event-xxx',
  499. 'hostname': 'xxx',
  500. 'foo': 'bar',
  501. })
  502. def test_survives_unknown_worker_leaving(self):
  503. s = State(on_node_leave=Mock(name='on_node_leave'))
  504. (worker, created), subject = s.event({
  505. 'type': 'worker-offline',
  506. 'hostname': 'unknown@vandelay.com',
  507. 'timestamp': time(),
  508. 'local_received': time(),
  509. 'clock': 301030134894833,
  510. })
  511. assert worker == Worker('unknown@vandelay.com')
  512. assert not created
  513. assert subject == 'offline'
  514. assert 'unknown@vandelay.com' not in s.workers
  515. s.on_node_leave.assert_called_with(worker)
  516. def test_on_node_join_callback(self):
  517. s = State(on_node_join=Mock(name='on_node_join'))
  518. (worker, created), subject = s.event({
  519. 'type': 'worker-online',
  520. 'hostname': 'george@vandelay.com',
  521. 'timestamp': time(),
  522. 'local_received': time(),
  523. 'clock': 34314,
  524. })
  525. assert worker
  526. assert created
  527. assert subject == 'online'
  528. assert 'george@vandelay.com' in s.workers
  529. s.on_node_join.assert_called_with(worker)
  530. def test_survives_unknown_task_event(self):
  531. s = State()
  532. s.event({
  533. 'type': 'task-unknown-event-xxx',
  534. 'foo': 'bar',
  535. 'uuid': 'x',
  536. 'hostname': 'y',
  537. 'timestamp': time(),
  538. 'local_received': time(),
  539. 'clock': 0,
  540. })
  541. def test_limits_maxtasks(self):
  542. s = State(max_tasks_in_memory=1)
  543. s.heap_multiplier = 2
  544. s.event({
  545. 'type': 'task-unknown-event-xxx',
  546. 'foo': 'bar',
  547. 'uuid': 'x',
  548. 'hostname': 'y',
  549. 'clock': 3,
  550. 'timestamp': time(),
  551. 'local_received': time(),
  552. })
  553. s.event({
  554. 'type': 'task-unknown-event-xxx',
  555. 'foo': 'bar',
  556. 'uuid': 'y',
  557. 'hostname': 'y',
  558. 'clock': 4,
  559. 'timestamp': time(),
  560. 'local_received': time(),
  561. })
  562. s.event({
  563. 'type': 'task-unknown-event-xxx',
  564. 'foo': 'bar',
  565. 'uuid': 'z',
  566. 'hostname': 'y',
  567. 'clock': 5,
  568. 'timestamp': time(),
  569. 'local_received': time(),
  570. })
  571. assert len(s._taskheap) == 2
  572. assert s._taskheap[0].clock == 4
  573. assert s._taskheap[1].clock == 5
  574. s._taskheap.append(s._taskheap[0])
  575. assert list(s.tasks_by_time())
  576. def test_callback(self):
  577. scratch = {}
  578. def callback(state, event):
  579. scratch['recv'] = True
  580. s = State(callback=callback)
  581. s.event({'type': 'worker-online'})
  582. assert scratch.get('recv')