└── trunk ├── .gitignore ├── COPYRIGHT ├── README ├── mbus--1.0--1.1.sql ├── mbus--1.0.sql ├── mbus--1.1.sql ├── mbus.control ├── migration.txt ├── prepare_for_dump.sql ├── readme.rus ├── security.txt └── tests ├── MBUSTest1.jar ├── readme.txt └── src └── name └── shaif └── MBUSTest1 ├── ArrangedMessageConsumer4MBUS.java ├── ArrangedMessageProducer4MBUS.java ├── ExceptionKind.java ├── Main.java ├── Message.java ├── MessageConsumer.java ├── MessageConsumer4MBUS.java ├── MessageConsumer4MBUSWithSelector.java ├── MessageProducer.java ├── MessageProducer4MBUS.java ├── ThreadFactory.java └── WrongCommandLineArgument.java /trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *.cmd 2 | src.sql 3 | qq 4 | out 5 | mbus4.sql -------------------------------------------------------------------------------- /trunk/COPYRIGHT: -------------------------------------------------------------------------------- 1 | Mbus is lightweight PostgreSQL extension for asynchronous messaging. 2 | 3 | Copyright (c) 2011-2012, Ivan Frolkov, Ilya Kosmodemiansky, PostgreSQL-Consulting.com 4 | 5 | Permission to use, copy, modify, and distribute this software and its 6 | documentation for any purpose, without fee, and without a written 7 | agreement is hereby granted, provided that the above copyright notice 8 | and this paragraph and the following two paragraphs appear in all 9 | copies. 10 | 11 | IN NO EVENT SHALL POSTGRESQL-CONSULTING.COM BE LIABLE TO ANY PARTY FOR 12 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 13 | INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND 14 | ITS DOCUMENTATION, EVEN IF POSTGRESQL-CONSULTING.COM HAS BEEN ADVISED 15 | OF THE POSSIBILITY OF SUCH DAMAGE. 16 | 17 | POSTGRESQL-CONSULTING.COM SPECIFICALLY DISCLAIMS ANY WARRANTIES, 18 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 20 | PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND 21 | POSTGRESQL-CONSULTING.COM HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, 22 | SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 23 | 24 | -------------------------------------------------------------------------------- /trunk/README: -------------------------------------------------------------------------------- 1 | for documentation please see project page http://code.google.com/p/mbus/ 2 | -------------------------------------------------------------------------------- /trunk/mbus--1.0--1.1.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE or replace FUNCTION clear_tempq() 3 | RETURNS void 4 | LANGUAGE plpgsql 5 | AS $$ 6 | begin 7 | delete from mbus.trigger where dst like 'temp.%' and not exists (select * from pg_stat_activity where dst like 'temp.' || md5(pid::text || backend_start::text) || '%'); 8 | delete from mbus.tempq where not exists (select * from pg_stat_activity where (headers->'tempq') is null and (headers->'tempq') like 'temp.' || md5(pid::text || backend_start::text) || '%'); 9 | end; 10 | $$; 11 | 12 | 13 | 14 | CREATE or replace FUNCTION create_temporary_queue() RETURNS text 15 | LANGUAGE sql 16 | AS $$ 17 | select 'temp.' || md5(pid::text || backend_start::text) || '.' || txid_current() from pg_stat_activity where pid=pg_backend_pid(); 18 | $$; 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /trunk/mbus--1.0.sql: -------------------------------------------------------------------------------- 1 | -- complain if script is sourced in psql, rather than via CREATE EXTENSION 2 | \echo Use "CREATE EXTENSION mbus" to load this file. \quit 3 | 4 | 5 | CREATE TABLE qt_model ( 6 | id integer NOT NULL, 7 | added timestamp without time zone NOT NULL, 8 | iid text NOT NULL, 9 | delayed_until timestamp without time zone NOT NULL, 10 | expires timestamp without time zone, 11 | received integer[], 12 | headers hstore, 13 | properties hstore, 14 | data hstore 15 | ); 16 | 17 | CREATE TABLE consumer ( 18 | id integer NOT NULL, 19 | name text, 20 | qname text, 21 | selector text, 22 | added timestamp without time zone 23 | -- constraint consumer_pkey PRIMARY KEY(id) 24 | ); 25 | 26 | 27 | 28 | CREATE SEQUENCE consumer_id_seq 29 | START WITH 1 30 | INCREMENT BY 1 31 | NO MINVALUE 32 | NO MAXVALUE 33 | CACHE 1; 34 | 35 | 36 | 37 | CREATE SEQUENCE qt_model_id_seq 38 | START WITH 1 39 | INCREMENT BY 1 40 | NO MINVALUE 41 | NO MAXVALUE 42 | CACHE 1; 43 | 44 | 45 | 46 | CREATE TABLE dmq ( 47 | id integer DEFAULT nextval('qt_model_id_seq'::regclass) NOT NULL, 48 | added timestamp without time zone NOT NULL, 49 | iid text NOT NULL, 50 | delayed_until timestamp without time zone NOT NULL, 51 | expires timestamp without time zone, 52 | received integer[], 53 | headers hstore, 54 | properties hstore, 55 | data hstore 56 | ); 57 | 58 | CREATE TABLE queue ( 59 | id integer NOT NULL, 60 | qname text NOT NULL, 61 | consumers_cnt integer 62 | ); 63 | 64 | 65 | 66 | CREATE SEQUENCE queue_id_seq 67 | START WITH 1 68 | INCREMENT BY 1 69 | NO MINVALUE 70 | NO MAXVALUE 71 | CACHE 1; 72 | 73 | 74 | 75 | CREATE SEQUENCE seq 76 | START WITH 1 77 | INCREMENT BY 1 78 | NO MINVALUE 79 | NO MAXVALUE 80 | CACHE 1; 81 | 82 | 83 | 84 | CREATE TABLE tempq ( 85 | id integer DEFAULT nextval('qt_model_id_seq'::regclass) NOT NULL, 86 | added timestamp without time zone NOT NULL, 87 | iid text NOT NULL, 88 | delayed_until timestamp without time zone NOT NULL, 89 | expires timestamp without time zone, 90 | received integer[], 91 | headers hstore, 92 | properties hstore, 93 | data hstore 94 | ); 95 | 96 | 97 | 98 | CREATE TABLE trigger ( 99 | src text NOT NULL, 100 | dst text NOT NULL, 101 | selector text 102 | ); 103 | 104 | 105 | 106 | 107 | ALTER TABLE ONLY consumer ALTER COLUMN id SET DEFAULT nextval('consumer_id_seq'::regclass); 108 | 109 | 110 | 111 | ALTER TABLE ONLY qt_model ALTER COLUMN id SET DEFAULT nextval('qt_model_id_seq'::regclass); 112 | 113 | 114 | 115 | ALTER TABLE ONLY queue ALTER COLUMN id SET DEFAULT nextval('queue_id_seq'::regclass); 116 | 117 | 118 | 119 | ALTER TABLE ONLY consumer 120 | ADD CONSTRAINT consumer_pkey PRIMARY KEY (id); 121 | 122 | 123 | 124 | ALTER TABLE ONLY dmq 125 | ADD CONSTRAINT dmq_iid_key UNIQUE (iid); 126 | 127 | 128 | 129 | ALTER TABLE ONLY qt_model 130 | ADD CONSTRAINT qt_model_iid_key UNIQUE (iid); 131 | 132 | 133 | 134 | ALTER TABLE ONLY queue 135 | ADD CONSTRAINT queue_pkey PRIMARY KEY (id); 136 | 137 | 138 | 139 | ALTER TABLE ONLY queue 140 | ADD CONSTRAINT queue_qname_key UNIQUE (qname); 141 | 142 | 143 | 144 | ALTER TABLE ONLY tempq 145 | ADD CONSTRAINT tempq_iid_key UNIQUE (iid); 146 | 147 | 148 | 149 | ALTER TABLE ONLY trigger 150 | ADD CONSTRAINT trigger_src_dst UNIQUE (src, dst); 151 | 152 | 153 | 154 | CREATE INDEX tempq_name_added ON tempq USING btree (((headers -> 'tempq'::text)), added) WHERE ((headers -> 'tempq'::text) IS NOT NULL); 155 | 156 | 157 | CREATE OR REPLACE FUNCTION string_format(format text, param hstore) 158 | RETURNS text 159 | LANGUAGE sql IMMUTABLE 160 | AS 161 | $BODY$ 162 | /* 163 | formats string using template 164 | %[name] - inserting quote_literal(param->'') 165 | %{name} - quote_ident 166 | 167 | select string_format('%[name] is %{value} and n=%[n] and name=%[name} %%[v]', hstore('name','lala') || hstore('value', 'The Value')||hstore('n',n::text)) from generate_series(1,1000) as gs(n); 168 | */ 169 | select 170 | array_to_string( 171 | array( 172 | select 173 | case when s[1] like '^%{%}' escape '^' 174 | then quote_ident($2->(substr(s[1],3,length(s[1])-3))) 175 | when s[1] like '^%[%]' escape '^' 176 | then quote_literal($2->(substr(s[1],3,length(s[1])-3))) 177 | when s[1] like '^%<%>' escape '^' 178 | then ($2->(substr(s[1],3,length(s[1])-3))) 179 | else 180 | s[1] 181 | end as s 182 | from regexp_matches($1, 183 | $RE$ 184 | ( 185 | % [[{<] \w+ []}>] 186 | | 187 | %% 188 | | 189 | (?: [^%]+ | %(?! [[{] ) ) 190 | ) 191 | $RE$, 192 | 'gx') 193 | as re(s) 194 | 195 | ), 196 | ''); 197 | 198 | $BODY$ 199 | COST 100; 200 | 201 | CREATE FUNCTION clear_tempq() 202 | RETURNS void 203 | LANGUAGE plpgsql 204 | AS $$ 205 | begin 206 | delete from mbus.trigger where dst like 'temp.%' and not exists (select * from pg_stat_activity where dst like 'temp.' || md5(procpid::text || backend_start::text) || '%'); 207 | delete from mbus.tempq where not exists (select * from pg_stat_activity where (headers->'tempq') is null and (headers->'tempq') like 'temp.' || md5(procpid::text || backend_start::text) || '%'); 208 | end; 209 | $$; 210 | 211 | 212 | 213 | 214 | 215 | CREATE FUNCTION consume(qname text, cname text DEFAULT 'default'::text) 216 | RETURNS SETOF qt_model 217 | LANGUAGE plpgsql 218 | AS $_$ 219 | begin 220 | if qname like 'temp.%' then 221 | return query select * from mbus.consume_temp(qname); 222 | return; 223 | end if; 224 | case lower(qname) 225 | when 'work' then case lower(cname) when 'default' then return query select * from mbus.consume_work_by_default(); return; when 'def2' then return query select * from mbus.consume_work_by_def2(); return; else raise exception $$unknown consumer:%$$, consumer; end case; 226 | 227 | end case; 228 | end; 229 | $_$; 230 | 231 | 232 | 233 | CREATE FUNCTION consume_temp(tqname text) 234 | RETURNS SETOF qt_model 235 | LANGUAGE plpgsql 236 | AS $$ 237 | declare 238 | rv mbus.qt_model; 239 | begin 240 | select * into rv from mbus.tempq where (headers->'tempq')=tqname and coalesce(expires,'2070-01-01'::timestamp)>now()::timestamp and coalesce(delayed_until,'1970-01-01'::timestamp)_by_() 268 | RETURNS SETOF mbus.qt_model AS 269 | $DDD$ 270 | declare 271 | rv mbus.qt_model; 272 | c_id int; 273 | pn int; 274 | cnt int; 275 | r record; 276 | gotrecord boolean:=false; 277 | begin 278 | set local enable_seqscan=off; 279 | 280 | if version() like 'PostgreSQL 9.0%' then 281 | for r in 282 | select * 283 | from mbus.qt$ t 284 | where <>all(received) and t.delayed_until)=true and added >'' and coalesce(expires,'2070-01-01'::timestamp) > now()::timestamp 285 | order by added, delayed_until 286 | limit 287 | loop 288 | begin 289 | select * into rv from mbus.qt$ t where t.iid=r.iid and <>all(received) for update nowait; 290 | continue when not found; 291 | gotrecord:=true; 292 | exit; 293 | exception 294 | when lock_not_available then null; 295 | end; 296 | end loop; 297 | else 298 | select * 299 | into rv 300 | from mbus.qt$ t 301 | where <>all(received) and t.delayed_until)=true and added > '' and coalesce(expires,'2070-01-01'::timestamp) > now()::timestamp 302 | and pg_try_advisory_xact_lock( ('X' || md5('mbus.qt$.' || t.iid))::bit(64)::bigint ) 303 | order by added, delayed_until 304 | limit 1 305 | for update; 306 | end if; 307 | 308 | 309 | if rv.iid is not null then 310 | if mbus.build__record_consumer_list(rv) <@ (rv.received || ) then 311 | delete from mbus.qt$ where iid = rv.iid; 312 | else 313 | update mbus.qt$ t set received=received || where t.iid=rv.iid; 314 | end if; 315 | rv.headers = rv.headers || hstore('destination',''); 316 | return next rv; 317 | return; 318 | end if; 319 | end; 320 | $DDD$ 321 | LANGUAGE plpgsql VOLATILE 322 | $CONS_SRC$; 323 | 324 | cons_src:=regexp_replace(cons_src,'', qname, 'g'); 325 | cons_src:=regexp_replace(cons_src,'', cname,'g'); 326 | cons_src:=regexp_replace(cons_src,'', (select consumers_cnt::text from mbus.queue q where q.qname=create_consumer.qname),'g'); 327 | cons_src:=regexp_replace(cons_src,'',c_id::text,'g'); 328 | cons_src:=regexp_replace(cons_src,'',selector,'g'); 329 | cons_src:=regexp_replace(cons_src,'',nowtime,'g'); 330 | cons_src:=regexp_replace(cons_src,'',c_id::text,'g'); 331 | execute cons_src; 332 | 333 | consn_src:=$CONSN_SRC$ 334 | ---------------------------------------------------------------------------------------- 335 | CREATE OR REPLACE FUNCTION mbus.consumen__by_(amt integer) 336 | RETURNS SETOF mbus.qt_model AS 337 | $DDD$ 338 | declare 339 | rv mbus.qt_model; 340 | rvarr mbus.qt_model[]; 341 | c_id int; 342 | pn int; 343 | cnt int; 344 | r record; 345 | inloop boolean; 346 | trycnt integer:=0; 347 | begin 348 | set local enable_seqscan=off; 349 | 350 | rvarr:=array[]::mbus.qt_model[]; 351 | if version() like 'PostgreSQL 9.0%' then 352 | while coalesce(array_length(rvarr,1),0) t 356 | where <>all(received) 357 | and t.delayed_until)=true 359 | and added > '' 360 | and coalesce(expires,'2070-01-01'::timestamp) > now()::timestamp 361 | and t.iid not in (select a.iid from unnest(rvarr) as a) 362 | order by added, delayed_until 363 | limit amt 364 | loop 365 | inloop:=true; 366 | begin 367 | select * into rv from mbus.qt$ t where t.iid=r.iid and <>all(received) for update nowait; 368 | continue when not found; 369 | rvarr:=rvarr||rv; 370 | exception 371 | when lock_not_available then null; 372 | end; 373 | end loop; 374 | exit when not inloop; 375 | end loop; 376 | else 377 | rvarr:=array(select row(t.* )::mbus.qt_model 378 | from mbus.qt$ t 379 | where <>all(received) 380 | and t.delayed_until)=true 382 | and added > '' 383 | and coalesce(expires,'2070-01-01'::timestamp) > now()::timestamp 384 | and t.iid not in (select a.iid from unnest(rvarr) as a) 385 | and pg_try_advisory_xact_lock( ('X' || md5('mbus.qt$.' || t.iid))::bit(64)::bigint ) 386 | order by added, delayed_until 387 | limit amt 388 | for update 389 | ); 390 | end if; 391 | 392 | if array_length(rvarr,1)>0 then 393 | for rv in select * from unnest(rvarr) loop 394 | if mbus.build__record_consumer_list(rv) <@ (rv.received || ) then 395 | delete from mbus.qt$ where iid = rv.iid; 396 | else 397 | update mbus.qt$ t set received=received || where t.iid=rv.iid; 398 | end if; 399 | end loop; 400 | return query select id, added, iid, delayed_until, expires, received, headers || hstore('destination','') as headers, properties, data from unnest(rvarr); 401 | return; 402 | end if; 403 | end; 404 | $DDD$ 405 | LANGUAGE plpgsql VOLATILE 406 | $CONSN_SRC$; 407 | 408 | consn_src:=regexp_replace(consn_src,'', qname, 'g'); 409 | consn_src:=regexp_replace(consn_src,'', cname,'g'); 410 | consn_src:=regexp_replace(consn_src,'', (select consumers_cnt::text from mbus.queue q where q.qname=create_consumer.qname),'g'); 411 | consn_src:=regexp_replace(consn_src,'',c_id::text,'g'); 412 | consn_src:=regexp_replace(consn_src,'',selector,'g'); 413 | consn_src:=regexp_replace(consn_src,'',nowtime,'g'); 414 | consn_src:=regexp_replace(consn_src,'',c_id::text,'g'); 415 | execute consn_src; 416 | 417 | take_src:=$TAKE$ 418 | create or replace function mbus.take_from__by_(msgid text) 419 | returns mbus.qt_model as 420 | $PRC$ 421 | update mbus.qt$ t set received=received || where iid=$1 and <> ALL(received) returning *; 422 | $PRC$ 423 | language sql; 424 | $TAKE$; 425 | take_src:=regexp_replace(take_src,'', qname, 'g'); 426 | take_src:=regexp_replace(take_src,'', cname,'g'); 427 | take_src:=regexp_replace(take_src,'', (select consumers_cnt::text from mbus.queue q where q.qname=create_consumer.qname),'g'); 428 | take_src:=regexp_replace(take_src,'',c_id::text,'g'); 429 | take_src:=regexp_replace(take_src,'',c_id::text,'g'); 430 | 431 | execute take_src; 432 | 433 | 434 | -- ind_src:= $IND$create index qt$_for_ on mbus.qt$((id % ), id, delayed_until) WHERE <> ALL (received) and ()=true and added >''$IND$; 435 | ind_src:= $IND$create index qt$_for_ on mbus.qt$(added, delayed_until) WHERE <> ALL (received) and ()=true and added >''$IND$; 436 | ind_src:=regexp_replace(ind_src,'', qname, 'g'); 437 | ind_src:=regexp_replace(ind_src,'', cname,'g'); 438 | ind_src:=regexp_replace(ind_src,'', (select consumers_cnt::text from mbus.queue q where q.qname=create_consumer.qname),'g'); 439 | ind_src:=regexp_replace(ind_src,'',c_id::text,'g'); 440 | ind_src:=regexp_replace(ind_src,'',selector,'g'); 441 | ind_src:=regexp_replace(ind_src,'',nowtime,'g'); 442 | if noindex then 443 | raise notice '%', 'You must create index ' || ind_src; 444 | else 445 | execute ind_src; 446 | end if; 447 | perform mbus.regenerate_functions(); 448 | end; 449 | $_$; 450 | 451 | 452 | CREATE FUNCTION create_queue(qname text, consumers_cnt integer) 453 | RETURNS void 454 | LANGUAGE plpgsql 455 | AS $_$ 456 | declare 457 | schname text:= 'mbus'; 458 | post_src text; 459 | clr_src text; 460 | peek_src text; 461 | begin 462 | execute 'create table ' || schname || '.qt$' || qname || '( like ' || schname || '.qt_model including all)'; 463 | insert into mbus.queue(qname,consumers_cnt) values(qname,consumers_cnt); 464 | post_src := $post_src$ 465 | CREATE OR REPLACE FUNCTION mbus.post_(data hstore, headers hstore DEFAULT NULL::hstore, properties hstore DEFAULT NULL::hstore, delayed_until timestamp without time zone DEFAULT NULL::timestamp without time zone, expires timestamp without time zone DEFAULT NULL::timestamp without time zone) 466 | RETURNS text AS 467 | $BDY$ 468 | notify QN_; 469 | select mbus.run_trigger('', $1, $2, $3, $4, $5); 470 | insert into mbus.qt$(data, 471 | headers, 472 | properties, 473 | delayed_until, 474 | expires, 475 | added, 476 | iid, 477 | received 478 | )values( 479 | $1, 480 | hstore('enqueue_time',now()::timestamp::text) || 481 | hstore('source_db', current_database()) || 482 | hstore('destination_queue', $Q$$Q$) || 483 | case when $2 is null then hstore('seenby','{' || current_database() || '}') else hstore('seenby', (($2->'seenby')::text[] || current_database()::text)::text) end, 484 | $3, 485 | coalesce($4, now() - '1h'::interval), 486 | $5, 487 | now(), 488 | current_database() || '.' || nextval('mbus.seq') || '.' || txid_current() || '.' || md5($1::text), 489 | array[]::int[] 490 | ) returning iid; 491 | $BDY$ 492 | LANGUAGE sql VOLATILE 493 | COST 100; 494 | $post_src$; 495 | post_src:=regexp_replace(post_src,'', qname, 'g'); 496 | execute post_src; 497 | 498 | clr_src:=$CLR_SRC$ 499 | create or replace function mbus.clear_queue_() 500 | returns void as 501 | $ZZ$ 502 | declare 503 | qry text; 504 | begin 505 | select string_agg( 'select id from mbus.consumer where id=' || id::text ||' and ( (' || r.selector || ')' || (case when r.added is null then ')' else $$ and q.added > '$$ || (r.added::text) || $$'::timestamp without time zone)$$ end) ||chr(10), ' union all '||chr(10)) 506 | into qry 507 | from mbus.consumer r where qname=''; 508 | execute 'delete from mbus.qt$ q where expires < now() or (received <@ array(' || qry || '))'; 509 | end; 510 | $ZZ$ 511 | language plpgsql 512 | $CLR_SRC$; 513 | 514 | clr_src:=regexp_replace(clr_src,'', qname, 'g'); 515 | execute clr_src; 516 | 517 | peek_src:=$PEEK$ 518 | create or replace function mbus.peek_(msgid text default null) 519 | returns boolean as 520 | $PRC$ 521 | select case 522 | when $1 is null then exists(select * from mbus.qt$) 523 | else exists(select * from mbus.qt$ where iid=$1) 524 | end; 525 | $PRC$ 526 | language sql 527 | $PEEK$; 528 | peek_src:=regexp_replace(peek_src,'', qname, 'g'); 529 | execute peek_src; 530 | 531 | perform mbus.create_consumer('default',qname); 532 | perform mbus.regenerate_functions(); 533 | end; 534 | $_$; 535 | 536 | 537 | 538 | CREATE FUNCTION create_run_function(qname text) 539 | RETURNS void 540 | LANGUAGE plpgsql 541 | AS $_$ 542 | declare 543 | func_src text:=$STR$ 544 | create or replace function mbus.run_on_(exec text) 545 | returns integer as 546 | $CODE$ 547 | declare 548 | r mbus.qt_model; 549 | cnt integer:=0; 550 | begin 551 | for r in select * from mbus.consumen__by_default(100) loop 552 | begin 553 | execute exec using r; 554 | cnt:=cnt+1; 555 | exception 556 | when others then 557 | insert into mbus.dmq(added, iid, delayed_until, expires,received, headers, properties, data) 558 | values(r.added,r.iid,r.delayed_until, r.expires,r.received,r.headers||hstore('dmq.added',now()::timestamp::text)||hstore('dmq.error',sqlerrm),r.properties,r.data); 559 | end; 560 | end loop; 561 | return cnt; 562 | end; 563 | $CODE$ 564 | language plpgsql 565 | $STR$; 566 | begin 567 | if not exists(select * from mbus.queue q where q.qname=create_run_function.qname) then 568 | raise exception 'Queue % does not exists!',qname; 569 | end if; 570 | func_src:=regexp_replace(func_src,'', qname, 'g'); 571 | execute func_src; 572 | raise notice '%', func_src; 573 | end; 574 | $_$; 575 | 576 | 577 | 578 | CREATE FUNCTION create_temporary_consumer(cname text, p_selector text DEFAULT NULL::text) 579 | RETURNS text 580 | LANGUAGE plpgsql 581 | AS $_$ 582 | declare 583 | selector text; 584 | tq text:=mbus.create_temporary_queue(); 585 | begin 586 | if not exists(select * from pg_catalog.pg_tables where schemaname='mbus' and tablename='qt$' || cname) then 587 | raise notice 'WARNING: source queue (%) does not exists (yet?)', src; 588 | end if; 589 | selector := case when p_selector is null or p_selector ~ '^ *$' then '1=1' else p_selector end; 590 | insert into mbus.trigger(src,dst,selector) values(cname,tq,selector); 591 | return tq; 592 | end; 593 | $_$; 594 | 595 | 596 | 597 | CREATE FUNCTION create_temporary_queue() RETURNS text 598 | LANGUAGE sql 599 | AS $$ 600 | select 'temp.' || md5(procpid::text || backend_start::text) || '.' || txid_current() from pg_stat_activity where procpid=pg_backend_pid(); 601 | $$; 602 | 603 | 604 | CREATE FUNCTION create_trigger(src text, dst text, selector text DEFAULT NULL::text) RETURNS void 605 | LANGUAGE plpgsql 606 | AS $_$ 607 | declare 608 | selfunc text; 609 | begin 610 | if not exists(select * from pg_catalog.pg_tables where schemaname='mbus' and tablename='qt$' || src) then 611 | raise notice 'WARNING: source queue (%) does not exists (yet?)', src; 612 | end if; 613 | if not exists(select * from pg_catalog.pg_tables where schemaname='mbus' and tablename='qt$' || dst) then 614 | raise notice 'WARNING: destination queue (%) does not exists (yet?)', dst; 615 | end if; 616 | 617 | if exists( 618 | with recursive tq as( 619 | select 1 as rn, src as src, dst as dst 620 | union all 621 | select tq.rn+1, t.src, t.dst from mbus.trigger t, tq where t.dst=tq.src 622 | ) 623 | select * from tq t1, tq t2 where t1.dst=t2.src and t1.rn=1 624 | ) then 625 | raise exception 'Loop detected'; 626 | end if; 627 | 628 | if selector is not null then 629 | begin 630 | execute $$with t as ( 631 | select now() as added, 632 | 'IID' as iid, 633 | now() as delayed_until, 634 | now() as expires, 635 | array[]::integer[] as received, 636 | hstore('$%$key','NULL') as headers, 637 | hstore('$%$key','NULL') as properties, 638 | hstore('$%$key','NULL') as data 639 | ) 640 | select * from t where $$ || selector; 641 | exception 642 | when sqlstate '42000' then 643 | raise exception 'Syntax error in selector:%', selector; 644 | end; 645 | 646 | selfunc:= $SRC$create or replace function mbus.trigger_(t mbus.qt_model) returns boolean as $FUNC$ 647 | begin 648 | return ; 649 | end; 650 | $FUNC$ 651 | language plpgsql immutable 652 | $SRC$; 653 | selfunc:=regexp_replace(selfunc,'', src || '_to_' || dst); 654 | selfunc:=regexp_replace(selfunc,'', selector); 655 | 656 | execute selfunc; 657 | end if; 658 | insert into mbus.trigger(src,dst,selector) values(src,dst,selector); 659 | end; 660 | $_$; 661 | 662 | 663 | 664 | CREATE FUNCTION create_view(qname text, cname text DEFAULT 'default'::text) RETURNS void 665 | LANGUAGE plpgsql 666 | AS $_$ 667 | declare 668 | param hstore:=hstore('qname',qname)||hstore('cname',cname); 669 | begin 670 | execute string_format($STR$ create view %_q as select data from mbus.consume('%', '%')$STR$, param); 671 | execute string_format($STR$ 672 | create or replace function trg_post_%() returns trigger as 673 | $thecode$ 674 | begin 675 | perform mbus.post('%',new.data); 676 | return null; 677 | end; 678 | $thecode$ 679 | security definer 680 | language plpgsql; 681 | 682 | create trigger trg_% instead of insert on %_q for each row execute procedure trg_post_%(); 683 | $STR$, param); 684 | end; 685 | $_$; 686 | 687 | 688 | CREATE FUNCTION create_view(qname text, cname text, sname text, viewname text) RETURNS void 689 | LANGUAGE plpgsql 690 | AS $_$ 691 | declare 692 | param hstore:=hstore('qname',qname)||hstore('cname',cname)|| hstore('sname',sname||'.')|| hstore('viewname', coalesce(viewname, 'public.'||qname)); 693 | begin 694 | execute string_format($STR$ create view %% as select data from mbus.consume('%', '%')$STR$, param); 695 | execute string_format($STR$ 696 | create or replace function %trg_post_%() returns trigger as 697 | $thecode$ 698 | begin 699 | perform mbus.post('%',new.data); 700 | return null; 701 | end; 702 | $thecode$ 703 | security definer 704 | language plpgsql; 705 | 706 | create trigger trg_% instead of insert on %% for each row execute procedure %trg_post_%(); 707 | $STR$, param); 708 | end; 709 | $_$; 710 | 711 | CREATE FUNCTION create_view_prop(qname text, cname text, sname text, viewname text) RETURNS void 712 | LANGUAGE plpgsql 713 | AS $_$ 714 | declare 715 | param hstore := hstore('qname',qname)||hstore('cname',cname)|| hstore('sname',sname||'.')|| hstore('viewname', coalesce(viewname, 'public.'||qname)); 716 | begin 717 | execute mbus.string_format($STR$ create view %% as select data, properties from mbus.consume('%', '%')$STR$, param); 718 | execute mbus.string_format($STR$ 719 | create or replace function %trg_post_%() returns trigger as 720 | $thecode$ 721 | begin 722 | perform mbus.post_%(new.data, null::hstore, new.properties, null::timestamp, null::timestamp); 723 | return null; 724 | end; 725 | $thecode$ 726 | security definer 727 | language plpgsql; 728 | 729 | create trigger trg_% instead of insert on %% for each row execute procedure %trg_post_%(); 730 | $STR$, param); 731 | end; 732 | $_$; 733 | 734 | create function queue_acl( oper text, usr text, qname text, viewname text, schemaname text) 735 | returns void 736 | language plpgsql as 737 | $_$ 738 | declare 739 | param hstore := hstore('oper', oper) || hstore('qname', qname) || hstore('usr', usr ) || hstore('viewname', viewname) || hstore('schemaname', schemaname); 740 | l_func text; 741 | begin 742 | if lower(oper) = 'grant' then 743 | param := param || hstore('dir', 'to'); 744 | elsif lower(oper) = 'revoke' then 745 | param := param || hstore('dir', 'from'); 746 | else 747 | return; 748 | end if; 749 | 750 | execute string_format($SCH$ % usage on schema mbus % %$SCH$, param); 751 | execute string_format($VSCH$ % usage on schema % % %$VSCH$, param); 752 | execute string_format($VIW$ % insert,select on %.% % %$VIW$, param); 753 | execute string_format($TBL$ % all on mbus.qt$% % %$TBL$, param); 754 | execute string_format($FNC$ 755 | with f as ( 756 | SELECT n.nspname || '.' || p.proname || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ')' as func 757 | FROM pg_catalog.pg_proc p 758 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace 759 | WHERE n.nspname ~ '^(mbus)$' and p.proname ~ '%' 760 | ) 761 | select array_to_string( array_agg(func), ',') from f 762 | $FNC$, param) into l_func; 763 | 764 | param := param || hstore('l_func', l_func); 765 | execute string_format($FNCL$% execute on function % % %$FNCL$, param); 766 | 767 | end; 768 | $_$; 769 | 770 | 771 | CREATE FUNCTION drop_consumer(cname text, qname text) RETURNS void 772 | LANGUAGE plpgsql 773 | AS $_$ 774 | begin 775 | delete from mbus.consumer c where c.name=drop_consumer.cname and c.qname=drop_consumer.qname; 776 | execute 'drop index mbus.qt$' || qname || '_for_' || cname; 777 | execute 'drop function mbus.consume_' || qname || '_by_' || cname || '()'; 778 | execute 'drop function mbus.consumen_' || qname || '_by_' || cname || '(integer)'; 779 | execute 'drop function mbus.take_from_' || qname || '_by_' || cname || '(text)'; 780 | perform mbus.regenerate_functions(); 781 | end; 782 | $_$; 783 | 784 | 785 | 786 | CREATE FUNCTION drop_queue(qname text) RETURNS void 787 | LANGUAGE plpgsql 788 | AS $_X$ 789 | declare 790 | r record; 791 | begin 792 | execute 'drop table mbus.qt$' || qname || ' cascade'; 793 | for r in (select * from 794 | pg_catalog.pg_proc prc, pg_catalog.pg_namespace nsp 795 | where prc.pronamespace=nsp.oid and nsp.nspname='mbus' 796 | and ( prc.proname ~ ('^post_' || qname || '$') 797 | or 798 | prc.proname ~ ('^consume_' || qname || $Q$_by_\w+$$Q$) 799 | or 800 | prc.proname ~ ('^consumen_' || qname || $Q$_by_\w+$$Q$) 801 | or 802 | prc.proname ~ ('^peek_' || qname || '$') 803 | or 804 | prc.proname ~ ('^take_from_' || qname) 805 | or 806 | prc.proname ~('^run_on_' || qname) 807 | ) 808 | ) loop 809 | case 810 | when r.proname like 'post_%' then 811 | execute 'drop function mbus.' || r.proname || '(hstore, hstore, hstore, timestamp without time zone, timestamp without time zone)'; 812 | when r.proname like 'consumen_%' then 813 | execute 'drop function mbus.' || r.proname || '(integer)'; 814 | when r.proname like 'peek_%' then 815 | execute 'drop function mbus.' || r.proname || '(text)'; 816 | when r.proname like 'take_%' then 817 | execute 'drop function mbus.' || r.proname || '(text)'; 818 | when r.proname like 'run_on_%' then 819 | execute 'drop function mbus.' || r.proname || '(text)'; 820 | else 821 | execute 'drop function mbus.' || r.proname || '()'; 822 | end case; 823 | end loop; 824 | delete from mbus.consumer c where c.qname=drop_queue.qname; 825 | delete from mbus.queue q where q.qname=drop_queue.qname; 826 | execute 'drop function mbus.clear_queue_' || qname || '()'; 827 | 828 | begin 829 | execute 'drop function mbus.build_' || qname || '_record_consumer_list(mbus.qt_model)'; 830 | exception when others then null; 831 | end; 832 | 833 | perform mbus.regenerate_functions(); 834 | end; 835 | $_X$; 836 | 837 | 838 | 839 | CREATE FUNCTION drop_trigger(src text, dst text) RETURNS void 840 | LANGUAGE plpgsql 841 | AS $$ 842 | begin 843 | delete from mbus.trigger where trigger.src=drop_trigger.src and trigger.dst=drop_trigger.dst; 844 | if found then 845 | begin 846 | execute 'drop function mbus.trigger_' || src || '_to_' || dst ||'(mbus.qt_model)'; 847 | exception 848 | when sqlstate '42000' then null; 849 | end; 850 | end if; 851 | end; 852 | $$; 853 | 854 | 855 | 856 | CREATE FUNCTION dyn_consume(qname text, selector text DEFAULT '(1=1)'::text, cname text DEFAULT 'default'::text) RETURNS SETOF qt_model 857 | LANGUAGE plpgsql 858 | AS $_$ 859 | declare 860 | rv mbus.qt_model; 861 | consid integer; 862 | consadded timestamp; 863 | hash text:='_'||md5(coalesce(selector,'')); 864 | begin 865 | set local enable_seqscan=off; 866 | if selector is null then 867 | selector:='(1=1)'; 868 | end if; 869 | select id, added into consid, consadded from mbus.consumer c where c.qname=dyn_consume.qname and c.name=dyn_consume.cname; 870 | begin 871 | execute 'execute /**/ mbus_dyn_consume_'||qname||hash||'('||consid||','''||consadded||''')' into rv; 872 | exception 873 | when sqlstate '26000' then 874 | execute 875 | $QRY$prepare mbus_dyn_consume_$QRY$ || qname||hash || $QRY$(integer, timestamp) as 876 | select * 877 | from mbus.qt$$QRY$ || qname ||$QRY$ t 878 | where $1<>all(received) and t.delayed_until $2 and coalesce(expires,'2070-01-01'::timestamp) > now()::timestamp 879 | and ($QRY$ || selector ||$QRY$) 880 | and pg_try_advisory_xact_lock( ('X' || md5('mbus.qt$$QRY$ || qname ||$QRY$.' || t.iid))::bit(64)::bigint ) 881 | order by added, delayed_until 882 | limit 1 883 | for update 884 | $QRY$; 885 | execute 'execute /**/ mbus_dyn_consume_'||qname||hash||'('||consid||','''||consadded||''')' into rv; 886 | end; 887 | 888 | 889 | if rv.iid is not null then 890 | if (select array_agg(id) from mbus.consumer c where c.qname=dyn_consume.qname and c.added<=rv.added) <@ (rv.received || consid::integer)::integer[] then 891 | begin 892 | execute 'execute mbus_dyn_delete_'||qname||'('''||rv.iid||''')' using qname; 893 | exception 894 | when sqlstate '26000' then 895 | execute 'prepare mbus_dyn_delete_'||qname||'(text) as delete from mbus.qt$'|| qname ||' where iid = $1'; 896 | execute 'execute mbus_dyn_delete_'||qname||'('''||rv.iid||''')'; 897 | end; 898 | else 899 | begin 900 | execute 'execute mbus_dyn_update_'||qname||'('''||rv.iid||''','||consid||')'; 901 | exception 902 | when sqlstate '26000' then 903 | execute 'prepare mbus_dyn_update_'||qname||'(text,integer) as update mbus.qt$'||qname||' t set received=received || $2 where t.iid=$1'; 904 | execute 'execute mbus_dyn_update_'||qname||'('''||rv.iid||''','||consid||')'; 905 | end; 906 | end if; 907 | rv.headers = rv.headers || hstore('destination',qname); 908 | return next rv; 909 | return; 910 | end if; 911 | end; 912 | $_$; 913 | 914 | 915 | 916 | CREATE FUNCTION post_temp(tqname text, data hstore, headers hstore DEFAULT NULL::hstore, properties hstore DEFAULT NULL::hstore, delayed_until timestamp without time zone DEFAULT NULL::timestamp without time zone, expires timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS text 917 | LANGUAGE sql 918 | AS $_$ 919 | insert into mbus.tempq(data, 920 | headers, 921 | properties, 922 | delayed_until, 923 | expires, 924 | added, 925 | iid, 926 | received 927 | )values( 928 | $2, 929 | hstore('tempq',$1) || 930 | hstore('enqueue_time',now()::timestamp::text) || 931 | hstore('source_db', current_database()) || 932 | case when $3 is null then hstore('seenby','{' || current_database() || '}') else hstore('seenby', (($3->'seenby')::text[] || current_database()::text)::text) end, 933 | $4, 934 | coalesce($5, now() - '1h'::interval), 935 | $6, 936 | now(), 937 | current_database() || '.' || nextval('mbus.seq') || '.' || txid_current() || '.' || md5($1::text), 938 | array[]::int[] 939 | ) returning iid; 940 | 941 | $_$; 942 | 943 | 944 | 945 | CREATE FUNCTION readme() RETURNS text 946 | LANGUAGE sql 947 | AS $_$ 948 | select $TEXT$ 949 | 950 | $TEXT$::text; 951 | $_$; 952 | CREATE FUNCTION readme_rus() RETURNS text 953 | LANGUAGE sql 954 | AS $_$ 955 | select $TEXT$ 956 | Управление очередями 957 | Очереди нормальные, полноценные, умеют 958 | 1. pub/sub 959 | 2. queue 960 | 3. request-response 961 | 962 | Еще умеют message selectorы, expiration и задержки доставки 963 | Payload'ом очереди является значение hstore (так что тип hstore должен быть установлен в базе) 964 | 965 | Очередь создается функцией 966 | mbus.create_queue(qname, ncons) 967 | где 968 | qname - имя очереди. Допустимы a-z (НЕ A-Z!), _, 0-9 969 | ncons - число одновременно доступных частей. Разумные значения - от 2 до 128-256 970 | больше ставить можно, но тогда будут слишком большие задержки на перебор всех частей 971 | 972 | Теперь в очередь можно помещать сообщения: 973 | select mbus.post_(data hstore, 974 | headers hstore DEFAULT NULL::hstore, 975 | properties hstore DEFAULT NULL::hstore, 976 | delayed_until timestamp without time zone DEFAULT NULL::timestamp without time zone, 977 | expires timestamp without time zone DEFAULT NULL::timestamp without time zone) 978 | где 979 | data - собственно payload 980 | headers - заголовки сообщения, в общем, не ожидается, что прикладная программа(ПП) будет их 981 | отправлять 982 | properties - заголовки сообщения, предназначенные для ПП 983 | delayed_until - сообщение будет доставлено ПОСЛЕ указанной даты. Зачем это надо? 984 | например, пытаемся отправить письмо, почтовая система недоступна. 985 | Тогда пишем куда-нибудь в properties число попыток, в delayed_until - (now()+'1h'::interval)::timestamp 986 | Через час это сообщение будет снова выбрано и снова предпринята попытка 987 | что-то сделать с сообщением 988 | expires - дата, до которой живет сообщение. По умолчанию - всегда. По достижению указанной даты сообщение удаляется 989 | Полезно, чтобы не забивать очереди всякой фигней типа "получить урл", а сеть полегла, 990 | сообщение было проигнорировано и так и осталось болтаться в очереди. 991 | От таких сообщений очередь чистится функцией mbus.clear_queue_() 992 | Возвращаемое значение: iid добавленного сообщения. 993 | 994 | и еще: 995 | mbus.post(qname text, data ...) 996 | Функция ничего не возвращает 997 | 998 | Получаем сообщения: 999 | mbus.consume(qname) - получает сообщения из очереди qname. Возвращает result set из одного 1000 | сообщения, колонки как в mbus.qt_model. Кроме описанных выше в post_, 1001 | существуют колонки: 1002 | id - просто id сообщения. Единственное, что про него можно сказать - оно уникально. 1003 | используется исключительно для генерирования id сообщения 1004 | iid - глобальное уникальное id сообщения. Предполагается, что оно глобально среди всех 1005 | сообщений; предполагается, что среди всех баз, обменивающихся сообщениями, каждая 1006 | имеет уникальное имя. 1007 | added - дата добавления сообщения в очередь 1008 | 1009 | Если сообщение было получено одним процессом вызовом функции mbus.consume(qname), то другой процесс 1010 | его НЕ ПОЛУЧИТ. Это классическая очередь. 1011 | 1012 | Реализация publish/subscribe 1013 | В настояшей реализации доступны только постоянные подписчики (durable subscribers). Подписчик создается 1014 | функцией 1015 | mbus.create_consumer(qname, cname, selector) 1016 | где 1017 | qname - имя очереди 1018 | cname - имя подписчика 1019 | selector - выражение, ограничивающее множество получаемых сообщений 1020 | Имя подписчика должно быть уникальным среди всех подписчиков (т.е. не только подписчиков этой очереди) 1021 | В selector допустимы только статические значения, известные на момент создания подписчика 1022 | Алиас выбираемой записи - t, тип - mbus.qt_model, т.е. селектор может иметь вид 1023 | $$(t.properties->'STATE')='DONE'$$, 1024 | но не 1025 | $$(t.properties>'user_posted')=current_user$$, 1026 | Следует заметить, что в настоящей реализации селекторы весьма эффективны и предпочтительней 1027 | пользоваться ими, чем фильтровать сообщения уже после получения. 1028 | Замечание: при создании очереди создается подписчик default 1029 | 1030 | Получение сообщений подписчиком: 1031 | mbus.consume(qname, cname) - возвращает набор типа mbus.qt_model из одной записи из очереди qname для подписчика cname 1032 | mbus.consume__by_() - см. выше 1033 | mbus.consumen__by_(amt integer) - получить не одно сообщение, а набор не более чем из amt штук. 1034 | 1035 | Сообщение msg, помещенное в очередь q, которую выбирают два процесса, получающие сообщения для подписчика 1036 | 'cons', будет выбрано только одним из двух процессов. Если эти процессы получают сообщения из очереди q для 1037 | подписчиков 'cons1' и 'cons2' соответственно, то каждый из них получит свою копию сообщения. 1038 | После получения поле headers сообщения содержит следующие сообщения: 1039 | seenby - text[], массив баз, которые получили это сообщение по пути к получаетелю 1040 | source_db - имя базы, в которой было создано данное сообщение 1041 | destination - имя очереди, из которой было получено это сообщение 1042 | enqueue_time - время помещения в очередь исходного сообщения (может отличаться от added, 1043 | которое указывает, в какое время сообщение было помещено в ту очередь, из которой происходит получение) 1044 | 1045 | Если сообщение не может быть получено, возвращается пустой набор. Почему не может быть получено сообщение? 1046 | Вариантов два: 1047 | 1. очередь просто пуста 1048 | 2. все выбираемые ветви очереди уже заняты подписчиками, получающими сообщения. Заняты они могут быть 1049 | как тем же подписчиком, так и другими. 1050 | 1051 | Всмпомогательные функции: 1052 | mbus.peek_(msgid text default null) - проверяет, если ли в очереди qname сообщение с iid=msgid 1053 | Если msgid is null, то проверяет наличие хоть какого-то сообщения. Следует учесть, что значение "истина", 1054 | возвращенное функцией peek, НЕ ГАРАНТИРУЕТ, что какие-либо функции из семейства consume вернут какое-либо 1055 | значение. 1056 | mbus.take_from__by_(msgid text) - получить из очереди qname сообщение с iid=msgid 1057 | ВНИМАНИЕ: это блокирующая функция, в случае, если запись с iid=msgid уже заблокирована какой-либо транзакцией, 1058 | эта функция будет ожидать доступности записи. 1059 | 1060 | 1061 | 1062 | Временные очереди. 1063 | Временная очередь создается функцией 1064 | mbus.create_temporary_queue() 1065 | Сообщения отправляются обычным mbus.post(qname, data...) 1066 | Сообщения получаются обычным mbus.consume(qname) 1067 | Временные очереди должны периодически очищаться от мусора вызовом функции 1068 | mbus.clear_tempq() 1069 | 1070 | Удаление очередей. 1071 | Временные очереди удалять не надо: они будут удалены автоматически после окончания сессии. 1072 | Обычные очереди удаляются функцией mbus.drop_queue(qname) 1073 | 1074 | Следует также обратить внимание на то, что активно используемые очереди должны _весьма_ 1075 | агрессивно очищаться (VACUUM) 1076 | 1077 | Триггеры 1078 | Для каждой очереди можно создать триггер - т.е. при поступлении сообщения в очередь 1079 | оно может быть скопировано в другую очередь, если селектор для триггера истинный. 1080 | Для чего это надо? Например, есть очень большая очередь, на которую потребовалось 1081 | подписаться. Создание еще одного подписчика - достаточно затратная вещь, для каждого 1082 | подписчика создается отдельный индекс; при большой очереди надо при создании подписчика 1083 | указывать параметр noindex - тогда индекс не будет создаваться, но текст запроса для 1084 | создания требуемого индекса будет возвращен как raise notice. 1085 | 1086 | create_run_function(qname text) 1087 | Генерирует функцию вида: 1088 | for r in select * from mbus.consumen__by_default(100) loop 1089 | execute exec using r; 1090 | end loop; 1091 | для указанной очереди. Используется для обработки сообщений внутри базы. 1092 | Сгенерированная фукция возвращает количество обработанных сообщений. 1093 | Если при обработке сообщения в exec возникло исключение, то сообщение помещается в dmq 1094 | 1095 | Функция mbus.create_view 1096 | Предполагается, что все функции выполняются от имени пользователя pg с соответствующими правами. 1097 | Это не всегда устраивает; данная фунцкция создает view с именем viewname (если не указано - то с именем public.queuename_q) 1098 | и триггер на вставку в него; на это view уже можно раздавать права для обычных пользователей. 1099 | 1100 | $TEXT$::text; 1101 | $_$; 1102 | 1103 | 1104 | 1105 | CREATE FUNCTION regenerate_functions() RETURNS void 1106 | LANGUAGE plpgsql 1107 | AS $_X$ 1108 | declare 1109 | r record; 1110 | r2 record; 1111 | post_qry text:=''; 1112 | consume_qry text:=''; 1113 | oldqname text:=''; 1114 | visibilty_qry text:=''; 1115 | begin 1116 | for r in select * from mbus.queue loop 1117 | post_qry:=post_qry || $$ when '$$ || lower(r.qname) || $$' then return mbus.post_$$ || r.qname || '(data, headers, properties, delayed_until, expires);'||chr(10); 1118 | end loop; 1119 | 1120 | if post_qry='' then 1121 | begin 1122 | execute 'drop function mbus.post(text, hstore, hstore, hstore, timestamp, timestamp)'; 1123 | exception when others then null; 1124 | end; 1125 | else 1126 | execute $FUNC$ 1127 | --------------------------------------------------------------------------------------------------------------------------------------------- 1128 | create or replace function mbus.post(qname text, data hstore, headers hstore default null, properties hstore default null, delayed_until timestamp default null, expires timestamp default null) 1129 | returns text as 1130 | $QQ$ 1131 | begin 1132 | if qname like 'temp.%' then 1133 | return mbus.post_temp(qname, data, headers, properties, delayed_until, expires); 1134 | end if; 1135 | case lower(qname) $FUNC$ || post_qry || $FUNC$ 1136 | else 1137 | raise exception 'Unknown queue:%', qname; 1138 | end case; 1139 | end; 1140 | $QQ$ 1141 | language plpgsql; 1142 | $FUNC$; 1143 | end if; 1144 | for r2 in select * from mbus.consumer order by qname loop 1145 | if oldqname<>r2.qname then 1146 | if consume_qry<>'' then 1147 | consume_qry:=consume_qry || ' else raise exception $$unknown consumer:%$$, cname; end case;' || chr(10); 1148 | end if; 1149 | consume_qry:=consume_qry || $$ when '$$ || lower(r2.qname) ||$$' then case lower(cname) $$; 1150 | end if; 1151 | consume_qry:=consume_qry || $$ when '$$ || lower(r2.name) || $$' then return query select * from mbus.consume_$$ || r2.qname || '_by_' || r2.name ||'(); return;'; 1152 | oldqname=r2.qname; 1153 | end loop; 1154 | 1155 | if consume_qry<>'' then 1156 | consume_qry:=consume_qry || ' else raise exception $$unknown consumer:%$$, consumer; end case;' || chr(10); 1157 | end if; 1158 | 1159 | if consume_qry='' then 1160 | begin 1161 | execute 'drop function mbus.consume(text, text)'; 1162 | exception when others then null; 1163 | end; 1164 | else 1165 | execute $FUNC$ 1166 | create or replace function mbus.consume(qname text, cname text default 'default') returns setof mbus.qt_model as 1167 | $QQ$ 1168 | begin 1169 | if qname like 'temp.%' then 1170 | return query select * from mbus.consume_temp(qname); 1171 | return; 1172 | end if; 1173 | case lower(qname) 1174 | $FUNC$ || consume_qry || $FUNC$ 1175 | end case; 1176 | end; 1177 | $QQ$ 1178 | language plpgsql; 1179 | $FUNC$; 1180 | end if; 1181 | 1182 | --create functions for tests for visibility 1183 | for r in 1184 | select string_agg( 1185 | 'select '|| id::text ||' from t where 1186 | ( (' 1187 | || cons.selector 1188 | || ')' 1189 | || (case when cons.added is null then ')' else $$ and t.added > '$$ 1190 | || (cons.added::text) 1191 | || $$'::timestamp without time zone)$$ end), 1192 | chr(10) || ' union all ' ||chr(10)) as src, 1193 | qname 1194 | from mbus.consumer cons 1195 | group by cons.qname 1196 | loop 1197 | execute $RCL$ 1198 | create or replace function mbus.build_$RCL$ || lower(r.qname) || $RCL$_record_consumer_list(qr mbus.qt_model) returns int[] as 1199 | $FUNC$ 1200 | begin 1201 | return array( 1202 | with t as (select qr.*) 1203 | $RCL$ || r.src || $RCL$ ); 1204 | end; 1205 | $FUNC$ 1206 | language plpgsql; 1207 | $RCL$; 1208 | end loop; 1209 | end; 1210 | $_X$; 1211 | 1212 | 1213 | 1214 | CREATE FUNCTION run_trigger(qname text, data hstore, headers hstore DEFAULT NULL::hstore, properties hstore DEFAULT NULL::hstore, delayed_until timestamp without time zone DEFAULT NULL::timestamp without time zone, expires timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS void 1215 | LANGUAGE plpgsql 1216 | AS $_$ 1217 | declare 1218 | r record; 1219 | res boolean; 1220 | qtm mbus.qt_model; 1221 | begin 1222 | if headers->'Redeploy' then 1223 | return; 1224 | end if; 1225 | <> 1226 | for r in select * from mbus.trigger t where src=qname loop 1227 | res:=false; 1228 | if r.selector is not null then 1229 | qtm.data:=data; 1230 | qtm.headers:=headers; 1231 | qtm.properties:=properties; 1232 | qtm.delayed_until:=delayed_until; 1233 | qtm.expires:=expires; 1234 | if r.dst like 'temp.%' then 1235 | perform mbus.post_temp(r.dst, data, headers, properties,delayed_until, expires); 1236 | else 1237 | begin 1238 | execute 'select mbus.trigger_'||r.src||'_to_'||r.dst||'($1)' into res using qtm; 1239 | exception 1240 | when others then 1241 | continue mainloop; 1242 | end; 1243 | end if; 1244 | continue mainloop when not res or res is null; 1245 | end if; 1246 | perform mbus.post(r.dst, data:=run_trigger.data, properties:=run_trigger.properties, headers:=run_trigger.headers); 1247 | end loop; 1248 | end; 1249 | $_$; 1250 | 1251 | 1252 | 1253 | CREATE FUNCTION trigger_work_to_jms_trigger_queue_testing(t qt_model) RETURNS boolean 1254 | LANGUAGE plpgsql IMMUTABLE 1255 | AS $$ 1256 | begin 1257 | return ((t.properties->'JMS_mbus-JMSType')='MAPMESSAGE'); 1258 | end; 1259 | $$; 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | -------------------------------------------------------------------------------- /trunk/mbus.control: -------------------------------------------------------------------------------- 1 | comment = 'asynchronous message bus' 2 | default_version = '1.1' 3 | encoding = 'utf-8' 4 | requires = 'hstore' 5 | superuser = true 6 | relocatable = false 7 | schema = 'mbus' 8 | 9 | -------------------------------------------------------------------------------- /trunk/migration.txt: -------------------------------------------------------------------------------- 1 | Для дампа очереди необходимо: 2 | 1. На источнике выполнить скрипт prepare_for_dump.sql 3 | 2. Получить дамп БД обычным pg_dump 4 | 3. В получателе создать прилагаемый extension с этой ветви 5 | 4. Влить дамп (при этом будет несколько ошибок "sequence already exists". Их можно проигнорировать) 6 | 5. По окончанию влива выполнить функцию mbus.regenerate_functions() 7 | 8 | Примечание: так как пользователи и их роли задаются на уровне всего кластера бд, 9 | пользователей и их права необходимо обрабатывать отдельно. Для облечения достижения этой цели можно 10 | воспользоваться командой: 11 | 12 | pg_dumpall -U postgres --roles-only >/tmp/roles 13 | -------------------------------------------------------------------------------- /trunk/prepare_for_dump.sql: -------------------------------------------------------------------------------- 1 | update pg_catalog.pg_extension set extconfig=t.config, extcondition=t.condition 2 | from ( 3 | select array_agg((schemaname || '.' || tablename)::regclass::oid) as config, 4 | array_agg(''::text) as condition 5 | from pg_catalog.pg_tables where schemaname='mbus' and tablename in ('qt_model','consumer','dmq','queue','trigger') 6 | ) as t 7 | where extname='mbus'; 8 | 9 | do $code$ 10 | declare 11 | r record; 12 | cons record; 13 | queue text; 14 | matches text[]; 15 | begin 16 | for r in (select substring(table_name from $RE$qt\$(.*)$RE$) as qname from information_schema.tables where table_schema='mbus' and table_name like 'qt$%') loop 17 | -- raise notice 'queue=%', r.qname; 18 | if not exists(select * from mbus.queue q where q.qname=r.qname) then 19 | insert into mbus.queue(qname, consumers_cnt) values(r.qname,128); 20 | end if; 21 | for cons in (select routine_definition as def, substring(ro.routine_name from '_by_(.*)') as cname 22 | from information_schema.routines ro 23 | where ro.specific_schema='mbus' and routine_name like 'consume_' || r.qname || '_by_%' 24 | ) loop 25 | raise notice ' consumer=%', cons.cname; 26 | matches = regexp_matches(cons.def, 27 | $RE$lock_not_available .* 28 | where \s+ (\d+)<>all\(received\) \s+ 29 | and \s+ t.delayed_until\s+ '(\d\d\d\d-\d\d-\d\d \s \d\d:\d\d:\d\d\.\d\d\d) .* 31 | .* limit \s+ 1 \s+ for \s+ update; $RE$,'x'); 32 | -- raise notice ' id=%', matches[1]; 33 | -- raise notice ' selector=%', matches[2]; 34 | -- raise notice ' added=%', matches[3]; 35 | if not exists(select * from mbus.consumer c where c.qname=r.qname and c.name=cons.cname) then 36 | insert into mbus.consumer(id, name, qname, selector,added) values(matches[1]::integer, cons.cname, r.qname, matches[2], matches[3]::timestamp); 37 | end if; 38 | end loop; 39 | end loop; 40 | perform setval('mbus.queue_id_seq', (select max(id) from mbus.queue)); 41 | perform setval('mbus.consumer_id_seq', (select max(id) from mbus.consumer)); 42 | end; 43 | $code$; 44 | 45 | do $code$ 46 | begin 47 | begin 48 | alter extension mbus drop sequence mbus.seq; 49 | exception 50 | when sqlstate '55000' then null; 51 | end; 52 | 53 | begin 54 | alter extension mbus drop sequence mbus.qt_model_id_seq; 55 | exception 56 | when sqlstate '55000' then null; 57 | end; 58 | 59 | begin 60 | alter extension mbus drop sequence mbus.consumer_id_seq; 61 | exception 62 | when sqlstate '55000' then null; 63 | end; 64 | 65 | begin 66 | alter extension mbus drop sequence mbus.queue_id_seq; 67 | exception 68 | when sqlstate '55000' then null; 69 | end; 70 | 71 | grant usage on schema mbus to public; 72 | end; 73 | $code$; -------------------------------------------------------------------------------- /trunk/readme.rus: -------------------------------------------------------------------------------- 1 |  Управление очередями 2 | 3 | Очереди нормальные, полноценные, умеют 4 | 1. pub/sub 5 | 2. queue 6 | 3. request-response 7 | 8 | Еще умеют message selectorы, expiration и задержки доставки 9 | Payload'ом очереди является значение hstore (так что тип hstore должен быть установлен в базе) 10 | 11 | Очередь создается функцией 12 | mbus.create_queue(qname, ncons, is_roles_security_model default false) 13 | где 14 | qname - имя очереди. Допустимы a-z (НЕ A-Z!), _, 0-9 15 | ncons - число одновременно доступных частей. Разумные значения - от 2 до 128-256 16 | больше ставить можно, но тогда будут слишком большие задержки на перебор всех частей 17 | Если указанная очередь уже существует, то будут пересозданы функции получения и отправки сообщений. 18 | Если параметр is_roles_security_model установлен, то право на отправку сообщений в очередь 19 | получат только те пользователи, у которых есть роль mbus__post_, а на получение 20 | обладатели роли mbus__consume__by_. 21 | 22 | Теперь в очередь можно помещать сообщения: 23 | select mbus.post_(data hstore, 24 | headers hstore DEFAULT NULL::hstore, 25 | properties hstore DEFAULT NULL::hstore, 26 | delayed_until timestamp without time zone DEFAULT NULL::timestamp without time zone, 27 | expires timestamp without time zone DEFAULT NULL::timestamp without time zone, 28 | iid text default null) 29 | где 30 | data - собственно payload 31 | headers - заголовки сообщения, в общем, не ожидается, что прикладная программа(ПП) будет их 32 | отправлять 33 | properties - заголовки сообщения, предназначенные для ПП 34 | delayed_until - сообщение будет доставлено ПОСЛЕ указанной даты. Зачем это надо? 35 | например, пытаемся отправить письмо, почтовая система недоступна. 36 | Тогда пишем куда-нибудь в properties число попыток, в delayed_until - (now()+'1h'::interval)::timestamp 37 | Через час это сообщение будет снова выбрано и снова предпринята попытка 38 | что-то сделать с сообщением 39 | expires - дата, до которой живет сообщение. По умолчанию - всегда. По достижению указанной даты сообщение удаляется 40 | Полезно, чтобы не забивать очереди всякой фигней типа "получить урл", а сеть полегла, 41 | сообщение было проигнорировано и так и осталось болтаться в очереди. 42 | От таких сообщений очередь чистится функцией mbus.clear_queue_() 43 | iid - id добавляемого сообщения. Должно быть получено с помощью функции mbus.get_iid 44 | Если не указано, то значение будет получено самостоятельно. 45 | 46 | !!! ВНИМАНИЕ !!! 47 | При отправке сообщения в очередь с iid, полученным для другой очереди, сообщение 48 | будет успешно отправлено, но в дальнейшем не будет доступно для функции mbus.take(iid). 49 | !!! ВНИМАНИЕ !!! 50 | 51 | Возвращаемое значение: iid добавленного сообщения. 52 | 53 | и еще: 54 | mbus.post(qname text, data ...) 55 | Функция ничего не возвращает 56 | 57 | Получаем сообщения: 58 | mbus.consume(qname) - получает сообщения из очереди qname. Возвращает result set из одного 59 | сообщения, колонки как в mbus.qt_model. Кроме описанных выше в post_, 60 | существуют колонки: 61 | id - просто id сообщения. Единственное, что про него можно сказать - оно уникально. 62 | используется исключительно для генерирования id сообщения 63 | iid - глобальное уникальное id сообщения. Предполагается, что оно глобально среди всех 64 | сообщений; предполагается, что среди всех баз, обменивающихся сообщениями, каждая 65 | имеет уникальное имя. 66 | added - дата добавления сообщения в очередь 67 | 68 | Если сообщение было получено одним процессом вызовом функции mbus.consume(qname), то другой процесс 69 | его НЕ ПОЛУЧИТ. Это классическая очередь. 70 | 71 | Реализация publish/subscribe 72 | В настояшей реализации доступны только как постоянные подписчики (durable subscribers), так и временные. 73 | Постоянный подписчик создается функцией 74 | mbus.create_consumer(qname, cname, selector) 75 | где 76 | qname - имя очереди 77 | cname - имя подписчика 78 | selector - выражение, ограничивающее множество получаемых сообщений 79 | Имя подписчика должно быть уникальным среди всех подписчиков (т.е. не только подписчиков этой очереди) 80 | В selector допустимы только статические значения, известные на момент создания подписчика 81 | Алиас выбираемой записи - t, тип - mbus.qt_model, т.е. селектор может иметь вид 82 | $$(t.properties->'STATE')='DONE'$$, 83 | но не 84 | $$(t.properties>'user_posted')=current_user$$, 85 | Следует заметить, что в настоящей реализации селекторы весьма эффективны и предпочтительней 86 | пользоваться ими, чем фильтровать сообщения уже после получения. 87 | Замечание: при создании очереди создается подписчик default 88 | 89 | Получение сообщений подписчиком: 90 | mbus.consume(qname, cname) - возвращает набор типа mbus.qt_model из одной записи из очереди qname для подписчика cname 91 | mbus.consume__by_() - см. выше 92 | mbus.consumen__by_(amt integer) - получить не одно сообщение, а набор не более чем из amt штук. 93 | 94 | Сообщение msg, помещенное в очередь q, которую выбирают два процесса, получающие сообщения для подписчика 95 | 'cons', будет выбрано только одним из двух процессов. Если эти процессы получают сообщения из очереди q для 96 | подписчиков 'cons1' и 'cons2' соответственно, то каждый из них получит свою копию сообщения. 97 | После получения поле headers сообщения содержит следующие сообщения: 98 | seenby - text[], массив баз, которые получили это сообщение по пути к получаетелю 99 | source_db - имя базы, в которой было создано данное сообщение 100 | destination - имя очереди, из которой было получено это сообщение 101 | enqueue_time - время помещения в очередь исходного сообщения (может отличаться от added, 102 | которое указывает, в какое время сообщение было помещено в ту очередь, из которой происходит получение) 103 | 104 | Если сообщение не может быть получено, возвращается пустой набор. Почему не может быть получено сообщение? 105 | Вариантов два: 106 | 1. очередь просто пуста 107 | 2. все выбираемые ветви очереди уже заняты подписчиками, получающими сообщения. Заняты они могут быть 108 | как тем же подписчиком, так и другими. 109 | 110 | Временные подписчики. 111 | Временный подписчик создается функцией select mbus.create_temporary_consumer(qname), 112 | или mbus.create_temporary_consumer(qname, selector), которая возвращает имя временной оченеди, 113 | куда и будут помещены копии сообщений. Следует иметь в виду, что это действительно копии, 114 | и, кроме того, в случае использования этого функционала должна периодически проводится очистка 115 | путем вызова функции mbus.clear_tempq() 116 | Подписчик существует до тех пор, пока активная текущая сессия. 117 | 118 | Всмпомогательные функции: 119 | 120 | mbus.peek_(msgid text default null) - проверяет, если ли в очереди qname сообщение с iid=msgid 121 | Если msgid is null, то проверяет наличие хоть какого-то сообщения. Следует учесть, что значение "истина", 122 | возвращенное функцией peek, НЕ ГАРАНТИРУЕТ, что какие-либо функции из семейства consume вернут какое-либо 123 | значение. 124 | mbus.take_from__by_(msgid text) - получить из очереди qname сообщение с iid=msgid 125 | ВНИМАНИЕ: это блокирующая функция, в случае, если запись с iid=msgid уже заблокирована какой-либо транзакцией, 126 | эта функция будет ожидать доступности записи. 127 | 128 | 129 | Временные очереди. 130 | Временная очередь создается функцией 131 | mbus.create_temporary_queue() 132 | Сообщения отправляются обычным mbus.post(qname, data...) 133 | Сообщения получаются обычным mbus.consume(qname) 134 | Временные очереди должны периодически очищаться от мусора вызовом функции 135 | mbus.clear_tempq() 136 | 137 | Выборка (consume) из временных очередей может быть блокирующей! 138 | 139 | Удаление очередей. 140 | Временные очереди удалять не надо: они будут удалены автоматически после окончания сессии. 141 | Обычные очереди удаляются функцией mbus.drop_queue(qname) 142 | 143 | Следует также обратить внимание на то, что активно используемые очереди должны _весьма_ 144 | агрессивно очищаться (VACUUM) 145 | 146 | Триггеры 147 | Для каждой очереди можно создать триггер - т.е. при поступлении сообщения в очередь 148 | оно может быть скопировано в другую очередь, если селектор для триггера истинный. 149 | Для чего это надо? Например, есть очень большая очередь, на которую потребовалось 150 | подписаться. Создание еще одного подписчика - достаточно затратная вещь, для каждого 151 | подписчика создается отдельный индекс; при большой очереди надо при создании подписчика 152 | указывать параметр noindex - тогда индекс не будет создаваться, но текст запроса для 153 | создания требуемого индекса будет возвращен как raise notice. 154 | Триггер создается фукцией mbus.create_trigger(src_queue_name text, dst_queue_name text, selector text); 155 | 156 | Функция mbus.create_view 157 | Предполагается, что все функции выполняются от имени пользователя pg с соответствующими правами. 158 | Это не всегда устраивает; данная фунцкция создает view с именем viewname (если не указано - то с именем public.queuename_q) 159 | и триггер на вставку в него; на это view уже можно раздавать права для обычных пользователей. 160 | 161 | 162 | Упорядочивание сообщений 163 | 164 | Для сообщения (назовем его 1) может быть указан id других сообщений(назовем их 2), ранее получения которых сообщение 1 не может быть получено. 165 | Он находится в заголовках и называется consume_after. Сообщения 1 и 2 не обязаны быть в одной очереди. Зачем это надо? 166 | Например, мы отправляем сообщение с командой "создать пользователя вася" и затем "для пользователя вася установить лимит в 10 тугриков". 167 | Так как порядок получения не определен, не исключена ситуация, когда сообщение с лимитом будет получено хронологически раньше, 168 | чем сообщение о создании пользователя. Таким образом, не очень понятно, что делать с сообщением об установлении лимита: 169 | либо отправить его обратно в очередь с увеличением счетчика получений и задержкой доставки, либо отбросить; в любом случае 170 | требуется дополнительный код и т.п. В случае же с упорядочиванием можно потребовать, чтобы сообщение с лимитом было получено 171 | только и исключительно после сообщения о создании; таким образом проблема устраняется. 172 | Так как сообщений о получении может быть указано несколько и они могут находиться в любой очереди, то вполне возможен такой 173 | вариант: 174 | поместить сообщение "создать пользователя" в очередь команды для сервера №1 и сохранить id сообщения как id1 175 | поместить сообщение "создать пользователя" в очередь команды для сервера №2 и сохранить id сообщения как id2 176 | ... 177 | поместить сообщение "создать пользователя" в очередь команды для сервера №N и сохранить id сообщения как idN 178 | 179 | поместить сообщение "установить лимит" с ограничением "получить после id1" в очередь команды для сервера №1 180 | поместить сообщение "установить лимит" с ограничением "получить после id2" в очередь команды для сервера №2 181 | ... 182 | поместить сообщение "установить лимит" с ограничением "получить после idN" в очередь команды для сервера №N 183 | 184 | поместить сообщение "установить местоположение профайла пользователя" с ограничением "получить после id1,id2,...idN" в очередь "локальные команды" 185 | и сохранить id сообщения как id_place_set 186 | поместить сообщение "удалить пользователя" с ограничением "получить после id_place_set" в очередь "локальные команды" 187 | 188 | Таким образом пользователь будет скопирован на сервера, на каждом из них будет установлен лимит, установлены ссылки на профайлы 189 | и удален пользователь на локальном сервере. 190 | 191 | !!!!! При невозможности обработать сообщение оно должно быть помещено обратно в ту же очередь или в dmq со старым iid !!!!!! 192 | 193 | Внимание! 194 | Большое количество сообщений(более нескольких тысяч), ожидающих доставки другого сообщения, может привести к снижению производительности. 195 | 196 | Упорядочивание сообщений работает независимо для каждого подписчика. 197 | 198 | Если сообщение стало недоступным по достижении указанного момента времени (expires), то все сообщения, зависящие от него, становятся 199 | доступны, но только в том случае, если у ставшего недоступным сообщения не указан заголовок 'zombie'. Если заголовок указан, 200 | то сообщение будет продолжать блокировать получение зависящиз от него, даже если истек срок доставки (now()>expires) 201 | 202 | 203 | Саги 204 | 205 | Сага - это долговременная последовательность транзакций, которая должна в итоге либо выполниться целиком, либо быть откачена целиком 206 | путем применения для каждой ранее успешно выполненной транзакции компенсационной транзакции, отменяющей ее действие. 207 | Например, создание тура для юзера - это сага (пример несколько искусственный, но тем не менее). Надо 208 | 209 | 1. Зарезервировать номер (например, через запрос какого-то стороннего вебсервиса) 210 | 2. Зарезервировать билеты на самолет (туда и обратно) исходя из параметров резервирования номера (какие-то http-запросы на сторонние сервера) 211 | 3. Исходя из параметров билетов - зарезервировать трансфер от/до аэропорта (еще какие-то http-запросы на другие сторонние сервера) 212 | 4. Юзер утверждает предложенный вариант через веб-интерфейс. 213 | 214 | Пункты 2 и 3 могут выполняться параллельно (как сами по себе - искать подходящие рейсы туда можно одновременно на нескольких сервисах; 215 | рейсы обратно можно искать также паралелльно; разумеется, эти два набора параллельных транзакций могут выполняться также параллельно). 216 | В случае сбоя любого из четырех пунктов (не удалось зарезервировать номер, не удалось зарезервировать самолеты или трансферы) 217 | все ранее проведенные резервирования должны быть отменены. 218 | 219 | Как это должно быть выполнено : 220 | 221 | Создаются ДВЕ последовательности действий, две упорядоченных группы сообщений : первая ("прямая") - резервирование 222 | и вторая, с компенсационными сообщениями("обратная") - откат резервирования, причем вторая упорядочена относительно 223 | сообщения в специальной очереди ("затычки"), из которой выборка производится только путем вызова функции take. 224 | Это служебное сообщение, от которого зависят компенсационные сообщения; кроме того, в нем находятся iid всех 225 | сообщений прямой последовательности; в случае инициирования отката это сообщение забирается функцией take и 226 | функцией же take забираются все сообщения прямой последовательности, полученные из этого служебного сообщения. 227 | Все сообщения прямой последовательности содержат ссылку на головное сообщение ("затычку") последовательности отката. 228 | Последнее сообщение прямой последовательности - также служебное, в нем находятся id сообщений последовательности отката и 229 | при окончании прямой последовательности они также забираются функцией take. Последнее сообщение обратной 230 | последовательности, как и прямой, служебное - для фиксации успешности проведения отката. 231 | 232 | 233 | Прямая последовательность | Откат 234 | ----------------------------------------------------------------------+----------------------------------------------------------------------- 235 | id выбрать после id команда Сообщение отката | id выбрать после id команда 236 | ----------------------------------------------------------------------+------------------------------------------------------------------------ 237 | 1 -/- Зарезервировать номер 1b | 1b забрать сообщения с #1,2,3,4,5,6 238 | могут выполняться / 2 1 Заказ билета туда 1b | 2b 1b отмена резервирования отеля \ 239 | параллельно \ 3 1 Заказ билета обратно 1b | 3b 1b отмена резервирования билета туда | 240 | могут выполняться / 4 2 Трансфер до отеля 1b | 4b 1b отмена резервирования билета обратно + могут выполняться параллельно 241 | параллельно \ 5 3 Трансфер от отеля 1b | 5b 1b отмена резервирования трансфера до отеля | 242 | 6 4,5 забрать сообщения с 1b | 6b 1b отмена резервирования транфера от отеля / 243 | #1b,2b,3b,4b,5b,6b,7b | 7b 2b,3b,4b,5b,6b откат завершен 244 | 245 | Всякий консумер прямой очереди (тот, который делает реальную работу - резервирует номер, рейсы, отправляет письмо "утвердите тур" и т.п.) в случае 246 | обнаружения необходимости отката выбирает сообщение-"затычку" путем вызова функции take(ид-сообщение-отката), откуда получает полный список 247 | сообщений прямой последотвальности, которые также забирает командой take, после чего фиксирует транзакцию и завершается. 248 | Если выбрать сообщение отката не удалось - не беда, стало быть, какой-то другой обработчик уже сделал это. 249 | Обработчик, удаляя главное сообщение отката ("затычку"), инициирует этим запуск обработчиков сообщений отката (которые аннулируют резервирование номера, 250 | авиабилетов и т.п.), а удалив сообщения прямой последовательности, останавливает прямую обработку сообщений (т.е. останавливает заказ авиабилетов, 251 | трансфера, всякие страховки и проч.) 252 | 253 | Для передачи данных между обработчиками нам потребуется иметь таблицу (или даже несколько таблиц) "проведение заказа тура" (тем более что все равно 254 | надо иметь возможность просмотреть формируемые туры, залипшие туры, отчетность по сформированным и т.п.) 255 | 256 | Каждый тип сообщения - как прямого, так и сообщения отката - фактически требует своего отдельного обработчика, что логично - 257 | обработчик, который резервирует номера, другой обработчик, который разбирается с авиабилетами, третий - с трансфером. 258 | 259 | Псевдокод : 260 | -- forward transactions 261 | bung_iid := get_iid('bungs'); -- нам потребуется iid сообщения-затычки 262 | final_iid := get_iid('final'); -- и финального сообщения в прямой последовательности 263 | 264 | insert into tour.... returning tour_id into tour_id; --в tour у нас будут жить промежуточные данные - номера рейсов, мест, номеров в отеле и и.п. 265 | --а также id сообщений, которые обрабатывают данный заказ на тур 266 | room_resvr := post('rooms_reservations', ....); 267 | book_tiket_to := post('book_tikets', consume_after := array[room_resvr], tour_id := tour_id, bung :=bung_iid ...); 268 | book_tiket_from := post('book_tikets', consume_after := array[room_resvr],tour_id := tour_id, bung :=bung_iid ...); 269 | transf_to := post('transfer_to', consume_after := array[book_tiket_to], tour_id := tour_id, bung :=bung_iid ...); 270 | transf_from := post('transfer_from', consume_after := array[book_tiket_from], tour_id := tour_id, bung :=bung_iid ...); 271 | 272 | -- compensation transactions 273 | post('bungs', id_to_take := array[ room_revrv, book_tiket_to, book_tiket_from, transf_to, transf_from, final_iid], iid:=bung_iid); 274 | cancel_room_resvr:= post('cancel_room_reservations', consume_after:=bung_iid, tour_id := tour_id...); 275 | cancel_tiket_booking_to:=post('cancel_tiket_booking_to', consume_after:=bung_iid, tour_id := tour_id...); 276 | cancel_tiket_booking_from:=post('cancel_tiket_booking_from', consume_after:=bung_iid, tour_id := tour_id...); 277 | cancel_transfer_to:=post('cancel_transfer_to', consume_after:=bung_iid, tour_id := tour_id...); 278 | cancel_transfer_from:=post('cancel_transfer_from', consume_after:=bung_iid, tour_id := tour_id...); 279 | post('cancel_completed', consume_after:=array[cancel_room_resvr,cancel_tiket_booking_to,canel_tiket_booking_from,canel_transfer_to,canel_transfer_from]); 280 | 281 | update tour set room_resvr_iid=room_resvr, 282 | .... 283 | cancel_room_resvr_iid:=cancel_room_resvr, 284 | .... 285 | where tour_id=tour_id; --чтобы можно было видеть в процессе выполнения статус обработки/отката резервирования тура 286 | 287 | --закрываем прямой ход 288 | post('end_of_tour_reservation', iid:=final_iid, id_to_take:=array[cancel_room_reservation,cancel_tiket_booking_to,canel_tiket_booking_from,canel_transfer_to,canel_transfer_from, bung_iid]); 289 | 290 | --наконец 291 | commit; 292 | 293 | Кроме того, нам потребуются 12 обработчиков сообщений - пять резверирующих номера, рейсы, трансфер и пять откатывающих резервирование, плюс два 294 | для окончания прямого хода или отката. Каждый из пяти резервантов должен в случае обнаружения необходимости отката выбрать сообщение с 295 | данным bung_id и уже из него выбрать iid сообщений из id_to_take и забрать их тоже. Два оставшихся обработчика - успешного выполнения и отката - 296 | должны соответствующим образом поменять запись в таблице tour - дескать, заказ завершен и отправлять сообщение с уведомлением 297 | юзера о том, что надо утвердить собранное (и менеджеру, что этот тур собран - ну или не собран, хехе) 298 | 299 | Что должны делать обработчики прямой и обратной последовательностей? Все очень прямолинейно: 300 | 1. Начать транзакцию 301 | 2. Получить сообщение 302 | 3. Попытаться выполнить требуемую операцию 303 | 3.1 При удачном выполнении - изменить запись о состоянии резервировании тура (всякие подробности в таблице tour) 304 | 3.2 При неудачном - либо отправить сообщение обратно с отложенной доставкой и увеличением счетчика, либо, если счетчик уже зашкаливает, перейти к следующему пункту 305 | 3.3 При невозможности - сервис посылает или слишком много попыток - начать откат, как описано выше - 306 | взять iid затычки, выбрать по нему затычку, выбрать все сообщения, которые в ней указаны, поменять tour на состояние "откатывается потому что...." 307 | 4. Зафиксировать транзакцию 308 | 309 | 310 | Безопасность и права доступа 311 | Разграничение прав для очередей может быть двух типов: 312 | 313 | 1. Обычное, как и в предыдущих версиях 314 | 2. С использованием штатных средств авторизации (GRANT/REVOKE) 315 | 316 | При создании очереди можно указать, как она создается: либо как и ранее, либо с указанием дополнительного параметра is_roles_security_model, 317 | установленного в true. При наличии этого параметра, во-первых, создаются функции отправки сообщения с опцией security definer, и, во-вторых, 318 | при отправке проверяется наличие роли mbus__post_ /где - имя базы, - имя очереди/ у пользователя, 319 | отправляющего сообщение. 320 | Для получения сообщения подписчик должен иметь роль вида mbus__consume__by_ / - имя подписчика, 321 | остальное см. выше/ 322 | При создании очереди (любой - как со старым способом ограничения доступа, так и с новым) автоматически создается соответсвующие роли - для отправки 323 | и для получения (при создании очереди автоматически создается получатель default - вот и роль для получения). 324 | При создании получателя (любого) автоматически создается соответствующая роль; эта роль удаляется при удалении получателя. 325 | При удалении очереди автоматически удаляются роли для отправки и получения сообщений (для всех получателей). 326 | -------------------------------------------------------------------------------- /trunk/security.txt: -------------------------------------------------------------------------------- 1 |  Безопасность и права доступа 2 | В предыдущих версиях неявно предполагалось, что все функции выполняются от суперпользователя. 3 | В настоящей версии это сохранено - по умолчанию поведение не изменилось. Однако у функции 4 | create_queue появился третий параметр: 5 | create_queue(qname text, consumers_cnt integer, is_roles_security_model boolean default null) 6 | Если этот параметр (то есть is_roles_security_model) истинный, то 7 | 1. Функции отправки и получения создаются как security definer 8 | 2. При отправке и получении сообщения проверяется наличие у отправляющего соответствующих ролей. 9 | Для отправки сообщения в очередь у пользователя должна быть роль вида 10 | mbus__post_ 11 | где - имя базы, в которой находится очередь 12 | - имя очереди, в которую отправляется сообщение. 13 | 14 | Для получения сообщения у пользователя должна быть роль вида 15 | mbus__consume__by_ 16 | где и имеют такой же смысл, как и выше, а - имя получателя; 17 | то есть пользователю дается роль получать сообщения из такой-то очереди для такого-то получателя. 18 | По умолчанию для каждой очередью вместе с ней самой создается и получатель default. 19 | 20 | Пример: 21 | select mbus.create_queue(qname:='mailq', consumer_cnt:=128, is_roles_security_model:=true); 22 | create role mbus_work_post_mailq; 23 | grant mbus_work_post_mailq to mail_sender; 24 | ... 25 | revoke mbus_work_post_mailq from mail_sender; 26 | 27 | -------------------------------------------------------------------------------- /trunk/tests/MBUSTest1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataegret/mbus/cd7d64e42b99b44b9f90242e4226f6e516597252/trunk/tests/MBUSTest1.jar -------------------------------------------------------------------------------- /trunk/tests/readme.txt: -------------------------------------------------------------------------------- 1 | java -jar MBUSTest1.jar 2 | Options are: 3 | -jdbc-url - test database jdbc url 4 | -producers - number of producer threads 5 | -consumers - number of consumer threads 6 | -messages - amount of messages to send for each producer thread -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/ArrangedMessageConsumer4MBUS.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.sql.*; 10 | 11 | /** 12 | * 13 | * @author if 14 | */ 15 | public class ArrangedMessageConsumer4MBUS extends MessageConsumer4MBUS{ 16 | final PreparedStatement insSth; 17 | final PreparedStatement seekSth; 18 | final PreparedStatement getMessageSth; 19 | private ArrangedMessageConsumer4MBUS(Connection conn, String qName) throws Exception{ 20 | super(conn, qName, null); 21 | conn.setAutoCommit(true); 22 | insSth = conn.prepareStatement("insert into result_arrivedMessage(msgid) values(?)"); 23 | seekSth = conn.prepareStatement("select msgid from result_arrivedMessage am where am.msgid=?"); 24 | getMessageSth = conn.prepareStatement("select ((headers->'consume_after')::text[])[1] as consume_after, data->'key' as key, iid as iid from mbus.consume('"+qName+"');"); 25 | } 26 | public static ArrangedMessageConsumer4MBUS CreateArrangedMessageConsumer4MBUS(Connection conn, String qName) throws Exception{ 27 | return new ArrangedMessageConsumer4MBUS(conn, qName); 28 | } 29 | @Override 30 | public Integer call() throws Exception{ 31 | Exception caughtException = null; 32 | boolean success = false; 33 | try{ 34 | if(conn.getAutoCommit()) 35 | conn.setAutoCommit(false); 36 | while(true){ 37 | getMessageSth.execute(); 38 | 39 | ResultSet rs = getMessageSth.getResultSet(); 40 | if(!rs.next()){ 41 | rs.close(); 42 | Thread.sleep(20); 43 | continue; 44 | } 45 | incrementMessagesReceived(); 46 | if(rs.getString("consume_after")==null){ 47 | insSth.setString(1, rs.getString("iid")); 48 | insSth.execute(); 49 | }else{ 50 | seekSth.setString(1, rs.getString("consume_after")); 51 | seekSth.execute(); 52 | ResultSet seekRs = seekSth.getResultSet(); 53 | try{ 54 | if(!seekRs.next() || !seekRs.getString("msgid").equals(rs.getString("consume_after"))){ 55 | System.out.println("Record not found:" + seekRs.getString("msgid") + " seek for" + rs.getString("consume_after")); 56 | throw new Exception(String.format("Get unordered message with iid=%s",rs.getString("iid"))); 57 | } 58 | }finally{ 59 | seekRs.close(); 60 | } 61 | } 62 | conn.commit(); 63 | rs.close(); 64 | success=true; 65 | } 66 | }catch(InterruptedException ex){ 67 | success=true; 68 | return getMessagesReceived(); 69 | }catch(SQLException ex){ 70 | caughtException = ex; 71 | System.err.println("SQL error:" + ex.getMessage()); 72 | throw new RuntimeException("SQL Error:" + ex.getMessage(), ex); 73 | }finally{ 74 | try{ 75 | if(!success) 76 | conn.rollback(); 77 | else 78 | conn.commit(); 79 | }catch(SQLException ex){ 80 | if(caughtException!=null) 81 | ex.initCause(caughtException); 82 | throw new RuntimeException("SQL Error:" + ex.getMessage(), ex); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/ArrangedMessageProducer4MBUS.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | import java.util.concurrent.Callable; 14 | 15 | /** 16 | * 17 | * @author if 18 | */ 19 | public class ArrangedMessageProducer4MBUS extends MessageProducer4MBUS{ 20 | final PreparedStatement firstPostSth; 21 | final PreparedStatement secondPostSth; 22 | 23 | protected ArrangedMessageProducer4MBUS(Connection conn, String qName, int messagesToSend) throws SQLException { 24 | super(conn, qName, messagesToSend,null); 25 | firstPostSth = conn.prepareStatement(String.format("select mbus.post('%s',hstore('key',?)) as iid", qName)); 26 | secondPostSth = conn.prepareStatement(String.format( 27 | "select mbus.post(qname:='%s',data:=hstore('key',?), headers:=hstore('consume_after',(array[?]::text)))", 28 | qName) 29 | ); 30 | } 31 | public static ArrangedMessageProducer4MBUS CreateArrangedMessageProducer4MBUS(Connection conn, String qName, int messagesToSend) throws SQLException{ 32 | return new ArrangedMessageProducer4MBUS(conn, qName, messagesToSend); 33 | } 34 | 35 | @Override 36 | public Integer call(){ 37 | Exception caughtException=null; 38 | int messagesSent=0; 39 | boolean success = false; 40 | try{ 41 | if(conn.getAutoCommit()) 42 | conn.setAutoCommit(false); 43 | while(messagesToSend>0){ 44 | if(Thread.currentThread().isInterrupted()) 45 | throw new InterruptedException("Interrupted"); 46 | firstPostSth.setString(1,java.lang.Integer.toString( messagesSent )); 47 | firstPostSth.execute(); 48 | ResultSet iidRs = firstPostSth.getResultSet(); 49 | if(!iidRs.next()) 50 | throw new RuntimeException("Cannot exec a post: have not got an iid"); 51 | secondPostSth.setString(1, java.lang.Integer.toString( messagesSent )); 52 | secondPostSth.setString(2, iidRs.getString("iid")); 53 | secondPostSth.execute(); 54 | messagesSent+=2; 55 | messagesToSend--; 56 | } 57 | success=true; 58 | conn.commit(); 59 | return messagesSent; 60 | }catch(InterruptedException ex){ 61 | return messagesSent; 62 | }catch(SQLException ex){ 63 | caughtException = ex; 64 | System.err.println("SQL Error:"+ex.getMessage()); 65 | throw new RuntimeException("SQL Error:" + ex.getMessage(), ex); 66 | }finally{ 67 | try{ 68 | if(!success) 69 | conn.rollback(); 70 | }catch(SQLException ex){ 71 | if(caughtException!=null) 72 | ex.initCause(caughtException); 73 | throw new RuntimeException("SQL Error: cannot commit:" + ex.getMessage(), ex); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/ExceptionKind.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | /** 10 | * 11 | * @author if 12 | */ 13 | public enum ExceptionKind { 14 | FATAL, SYSTEM, RETRIABLE, USER 15 | } 16 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.nio.ByteBuffer; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.sql.*; 12 | import java.util.*; 13 | import java.util.concurrent.*; 14 | 15 | /** 16 | * 17 | * @author if 18 | * Main class for test mbus for correctness and performance 19 | */ 20 | public class Main{ 21 | static int TOTAL_PRODUCERS = 20; 22 | static int TOTAL_CONSUMERS = 7; 23 | static int TOTAL_TO_SEND = 1000; 24 | 25 | public static int getTOTAL_TO_SEND() { 26 | return TOTAL_TO_SEND; 27 | } 28 | static final String SELECTOR="((data->'key')::integer=100)"; 29 | 30 | public static int getTOTAL_PRODUCERS() { 31 | return TOTAL_PRODUCERS; 32 | } 33 | 34 | public static int getTOTAL_CONSUMERS() { 35 | return TOTAL_CONSUMERS; 36 | } 37 | 38 | public static String getSELECTOR() { 39 | return SELECTOR; 40 | } 41 | 42 | public static String getNOT_SELECTOR() { 43 | return NOT_SELECTOR; 44 | } 45 | 46 | public static String getJDBC_URL() { 47 | return JDBC_URL; 48 | } 49 | 50 | public static String getJDBC_USER() { 51 | return JDBC_USER; 52 | } 53 | 54 | public static String getJDBC_PASSWORD() { 55 | return JDBC_PASSWORD; 56 | } 57 | static final String NOT_SELECTOR = "not(" + SELECTOR + ")"; 58 | static String JDBC_URL = "jdbc:postgresql://localhost:5433/dst?prepareThreshold=1"; 59 | static String JDBC_USER = "postgres"; 60 | static String JDBC_PASSWORD = "root"; 61 | 62 | private static void ParseCommandLine(String args[]) throws Exception{ 63 | Map cmdLine = new HashMap(); 64 | boolean first=true; 65 | String key = null; 66 | for(String s : args){ 67 | if(first){ 68 | if(s.matches("^[^-]")) 69 | throw new WrongCommandLineArgument("Wrong switch:" + s); 70 | key = s; 71 | first = false; 72 | if(s.equals("-help")) 73 | cmdLine.put(s,"-help"); 74 | }else{ 75 | assert key!=null; 76 | cmdLine.put(key, s); 77 | first = true; 78 | } 79 | } 80 | for(String s: cmdLine.keySet()){ 81 | if(s.equals("-jdbc-url")) 82 | JDBC_URL = cmdLine.get(s); 83 | else if(s.equals("-jdbc-user")) 84 | JDBC_USER = cmdLine.get(s); 85 | else if(s.equals("-jdbc-password")) 86 | JDBC_PASSWORD = cmdLine.get(s); 87 | else if(s.equals("-producers")){ 88 | TOTAL_PRODUCERS = Integer.parseInt(cmdLine.get(s)); 89 | assert TOTAL_PRODUCERS > 0; 90 | } else if(s.equals("-consumers")){ 91 | TOTAL_CONSUMERS = Integer.parseInt(cmdLine.get(s)); 92 | assert TOTAL_CONSUMERS > 0; 93 | } else if(s.equals("-messages")){ 94 | TOTAL_TO_SEND = Integer.parseInt(cmdLine.get(s)); 95 | assert TOTAL_TO_SEND > 0; 96 | } else if(s.equals("-help")){ 97 | System.out.println("Usage: java -jar MBUSTest1.jar []\n" 98 | + "\t[-jdbc-url ]\n" 99 | + "\t[-jdbc-user ]\n" 100 | + "\t[-jdbc-password ]\n" 101 | + "\t[-producers ]\n" 102 | + "\t[-consumers \n" 103 | + "\t[-help]"); 104 | throw new ShowHelpException(); 105 | }else 106 | throw new WrongCommandLineArgument("Unknown switch:"+s); 107 | } 108 | } 109 | 110 | public static void checkSentAndReceived(int sentCnt, int receivedCnt){ 111 | System.out.format("Expected:%d consumed:%d", sentCnt, receivedCnt); 112 | if(sentCnt==receivedCnt) 113 | System.out.println(" +++ OK"); 114 | else{ 115 | System.out.format(" *** ERROR Sent:%d Received:%d", sentCnt, receivedCnt); 116 | } 117 | } 118 | 119 | public static void main(String[] args) throws Exception{ 120 | try{ 121 | ParseCommandLine(args); 122 | }catch(ShowHelpException ex){ 123 | return; 124 | }catch(Exception e){ 125 | System.err.println(e.getMessage()); 126 | return; 127 | } 128 | System.out.println("Plain queues test"); 129 | long start1 = System.currentTimeMillis(); 130 | mainMBUS(); 131 | long end1 = System.currentTimeMillis(); 132 | System.out.println("Plain queues OK:" + (end1-start1)); 133 | System.out.println("Selector & consumer queues test"); 134 | long start2 = System.currentTimeMillis(); 135 | mainMBUSSelector(); 136 | long end2 = System.currentTimeMillis(); 137 | System.out.println("Selector & consumer queues test OK:" + (end2-start2)); 138 | System.out.println("Arranged test"); 139 | long start3 = System.currentTimeMillis(); 140 | mainMBUSArranged(); 141 | long end3 = System.currentTimeMillis(); 142 | System.out.println("Arranged test OK:"+(end3-start3)); 143 | } 144 | 145 | static private String byteArrayToHex(byte[] a) { 146 | StringBuilder sb = new StringBuilder(); 147 | for(byte b: a) 148 | sb.append(String.format("%02x", b&0xff)); 149 | return sb.toString(); 150 | } 151 | /** 152 | * Makes test for orderer messages. 153 | * @throws Exception 154 | */ 155 | public static void mainMBUSArranged() throws Exception{ 156 | String qName = "arrangedq" + byteArrayToHex(java.security.MessageDigest.getInstance("MD5").digest(ByteBuffer.allocate(8).putLong(Thread.currentThread().getId()).array())); 157 | qName = qName.substring(0, 31); 158 | 159 | Properties props = new Properties(); 160 | props.setProperty("user",getJDBC_USER()); 161 | props.setProperty("password",getJDBC_PASSWORD()); 162 | String jdbcUrl = JDBC_URL; 163 | Connection conn = DriverManager.getConnection(getJDBC_URL(), props); 164 | conn.setAutoCommit(false); 165 | Statement preparationSth = conn.createStatement(); 166 | preparationSth.execute("select mbus.create_queue('"+qName+"',256);"); 167 | preparationSth.execute("drop table if exists result_arrivedMessage"); 168 | preparationSth.execute("create table result_arrivedMessage(msgid text primary key)"); 169 | conn.commit(); 170 | 171 | try{ 172 | ThreadFactory producerThreads = new ThreadFactory(); 173 | ExecutorService producerExec = Executors.newFixedThreadPool(getTOTAL_PRODUCERS(), producerThreads); 174 | List> producersResults = new ArrayList>(); 175 | 176 | ThreadFactory consumerThreads = new ThreadFactory(); 177 | ExecutorService consumerExec = Executors.newFixedThreadPool(getTOTAL_CONSUMERS(),consumerThreads); 178 | List> consumersResults = new ArrayList>(); 179 | int i; 180 | 181 | for(i=0;i f: producersResults) 189 | totalSend+=f.get(); 190 | 191 | conn.setAutoCommit(true); 192 | PreparedStatement sth=conn.prepareStatement("select 1 as has from mbus.qt$" + qName + " limit 1"); 193 | while(true){ 194 | sth.execute(); 195 | ResultSet rs = sth.getResultSet(); 196 | boolean rsnext = rs.next(); 197 | rs.close(); 198 | if(!rsnext) 199 | break; 200 | Thread.sleep(20); 201 | } 202 | System.out.println("All received"); 203 | 204 | 205 | for(Thread thr: consumerThreads.getThreads()) 206 | thr.interrupt(); 207 | 208 | int totalReceived=0; 209 | for(Future f: consumersResults) 210 | totalReceived+=f.get(); 211 | 212 | producerExec.shutdown(); 213 | consumerExec.shutdown(); 214 | 215 | checkSentAndReceived(totalSend, totalReceived); 216 | }finally{ 217 | conn.setAutoCommit(false); 218 | preparationSth.executeQuery("select mbus.drop_queue('" + qName +"')"); 219 | conn.commit(); 220 | } 221 | } 222 | 223 | /** 224 | * Makes test for several subscribers with two 225 | * @throws InterruptedException 226 | * @throws ExecutionException 227 | * @throws SQLException 228 | * @throws NoSuchAlgorithmException 229 | */ 230 | public static void mainMBUSSelector() throws InterruptedException, ExecutionException, SQLException, NoSuchAlgorithmException{ 231 | String qName = "testq" + byteArrayToHex(java.security.MessageDigest.getInstance("MD5").digest(ByteBuffer.allocate(8).putLong(Thread.currentThread().getId()).array())); 232 | qName = qName.substring(0, 31); 233 | System.out.println(qName); 234 | 235 | Properties props = new Properties(); 236 | props.setProperty("user",getJDBC_USER()); 237 | props.setProperty("password",getJDBC_PASSWORD()); 238 | 239 | Connection initConn = DriverManager.getConnection(getJDBC_URL(), props); 240 | initConn.setAutoCommit(false); 241 | Statement preparationSth = initConn.createStatement(); 242 | try{ 243 | preparationSth.execute("select mbus.drop_queue('"+qName+"');"); 244 | }catch(SQLException sqlEx){ 245 | if(!sqlEx.getSQLState().equals("42P01")) 246 | throw sqlEx; 247 | initConn.rollback(); 248 | } 249 | preparationSth.execute("select mbus.create_queue('"+qName+"',256);"); 250 | initConn.commit(); 251 | try{ 252 | preparationSth.execute(String.format("select mbus.create_consumer('cons1','%s',$STR$%s$STR$);",qName, getSELECTOR())); 253 | preparationSth.execute(String.format("select mbus.create_consumer('cons2','%s',$STR$%s$STR$);",qName, getNOT_SELECTOR())); 254 | initConn.commit(); 255 | 256 | ThreadFactory producerThreads = new ThreadFactory(); 257 | ExecutorService producerExec = Executors.newFixedThreadPool(getTOTAL_PRODUCERS(),producerThreads); 258 | List> producersResults = new ArrayList>(); 259 | 260 | ThreadFactory consumerThreads = new ThreadFactory(); 261 | ExecutorService consumerExec = Executors.newFixedThreadPool(getTOTAL_CONSUMERS()*3, consumerThreads); 262 | List> consumersResults = new ArrayList>(); 263 | int i; 264 | 265 | for(i=0;i f: producersResults) 276 | totalSend+=f.get(); 277 | 278 | producerExec.shutdown(); 279 | consumerExec.shutdown(); 280 | 281 | initConn.setAutoCommit(true); 282 | PreparedStatement sth=initConn.prepareStatement("select 1 as has from mbus.qt$" + qName + " limit 1"); 283 | while(true){ 284 | sth.execute(); 285 | ResultSet rs = sth.getResultSet(); 286 | boolean rsnext = rs.next(); 287 | rs.close(); 288 | if(!rsnext) 289 | break; 290 | Thread.sleep(20); 291 | } 292 | 293 | 294 | for(Thread thr: consumerThreads.getThreads()) 295 | thr.interrupt(); 296 | 297 | int totalReceived=0; 298 | for(Future f: consumersResults) 299 | totalReceived+=f.get(); 300 | 301 | checkSentAndReceived(2*totalSend, totalReceived); 302 | }catch(SQLException e){ 303 | System.err.println(e.getMessage()); 304 | throw new RuntimeException(e.getMessage(), e); 305 | }finally{ 306 | initConn.setAutoCommit(false); 307 | preparationSth.executeQuery("select mbus.drop_queue('" + qName +"')"); 308 | initConn.commit(); 309 | } 310 | } 311 | /** 312 | * 313 | * @throws InterruptedException 314 | * @throws ExecutionException 315 | * @throws SQLException 316 | * @throws NoSuchAlgorithmException 317 | */ 318 | public static void mainMBUS() throws InterruptedException, ExecutionException, SQLException, NoSuchAlgorithmException{ 319 | String qName = "mainq" + byteArrayToHex(java.security.MessageDigest.getInstance("MD5").digest(ByteBuffer.allocate(8).putLong(Thread.currentThread().getId()).array())); 320 | qName = qName.substring(0, 31); 321 | 322 | Properties props = new Properties(); 323 | props.setProperty("user",getJDBC_USER()); 324 | props.setProperty("password",getJDBC_PASSWORD()); 325 | String jdbcUrl = getJDBC_URL(); 326 | Connection initConn = DriverManager.getConnection(getJDBC_URL(), props); 327 | initConn.setAutoCommit(false); 328 | Statement preparationSth = initConn.createStatement(); 329 | try{ 330 | preparationSth.execute("select mbus.drop_queue('"+qName+"');"); 331 | }catch(SQLException sqlEx){ 332 | if(!sqlEx.getSQLState().equals("42P01")) 333 | throw sqlEx; 334 | initConn.rollback(); 335 | } 336 | preparationSth.execute("select mbus.create_queue('"+qName+"',256);"); 337 | initConn.commit(); 338 | ExecutorService producerExec=null; 339 | ExecutorService consumerExec=null; 340 | try{ 341 | for(int i=1; i<=getTOTAL_PRODUCERS();i++){ 342 | Connection conn = DriverManager.getConnection(getJDBC_URL(), props); 343 | 344 | ThreadFactory producerThreads = new ThreadFactory(); 345 | producerExec = Executors.newFixedThreadPool(getTOTAL_PRODUCERS(), producerThreads); 346 | List> producersResults = new ArrayList>(); 347 | 348 | ThreadFactory consumerThreads = new ThreadFactory(); 349 | consumerExec = Executors.newFixedThreadPool(getTOTAL_CONSUMERS(), consumerThreads); 350 | List> consumersResults = new ArrayList>(); 351 | 352 | System.out.println("Going to work"); 353 | 354 | for(i=0;i f: producersResults) 366 | totalSend+=f.get(); 367 | 368 | conn.setAutoCommit(true); 369 | PreparedStatement sth=conn.prepareStatement("select 1 as has from mbus.qt$" + qName + " limit 1"); 370 | while(true){ 371 | sth.execute(); 372 | ResultSet rs = sth.getResultSet(); 373 | boolean rsnext = rs.next(); 374 | rs.close(); 375 | if(!rsnext) 376 | break; 377 | Thread.sleep(10); 378 | } 379 | System.out.println("All received"); 380 | 381 | for(Thread thr: consumerThreads.getThreads()) 382 | thr.interrupt(); 383 | 384 | int totalReceived=0; 385 | for(Future f: consumersResults) 386 | totalReceived+=f.get(); 387 | 388 | checkSentAndReceived(totalSend, totalReceived); 389 | } 390 | }finally{ 391 | initConn.setAutoCommit(false); 392 | preparationSth.executeQuery("select mbus.drop_queue('" + qName +"')"); 393 | initConn.commit(); 394 | if(producerExec!=null) 395 | producerExec.shutdown(); 396 | 397 | if(consumerExec!=null) 398 | consumerExec.shutdown(); 399 | } 400 | } 401 | public static void mainBlockingQueue() throws InterruptedException, ExecutionException{ 402 | BlockingQueue q = new ArrayBlockingQueue(10, true); 403 | 404 | ThreadFactory producerThreads = new ThreadFactory(); 405 | ExecutorService producerExec = Executors.newCachedThreadPool(producerThreads); 406 | List> producersResults = new ArrayList>(); 407 | 408 | ThreadFactory consumerThreads = new ThreadFactory(); 409 | ExecutorService consumerExec = Executors.newCachedThreadPool(consumerThreads); 410 | List> consumersResults = new ArrayList>(); 411 | 412 | for(int i=0;i f: producersResults) 423 | totalSend+=f.get(); 424 | 425 | int totalReceived=0; 426 | 427 | for(Thread thread : consumerThreads.getThreads()) 428 | thread.interrupt(); 429 | 430 | for(Future f: consumersResults) 431 | totalReceived+=f.get(); 432 | 433 | checkSentAndReceived(totalSend, totalReceived); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/Message.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Calendar; 11 | import java.util.GregorianCalendar; 12 | 13 | /** 14 | * 15 | * @author if 16 | */ 17 | public class Message { 18 | final private String description; 19 | final private BigDecimal amount; 20 | final private Calendar added; 21 | 22 | private Message(String description, BigDecimal amount, Calendar added){ 23 | this.description = description; 24 | this.amount = amount; 25 | this.added = added; 26 | } 27 | 28 | static public Message CreateMessage(String description, BigDecimal amount, Calendar added){ 29 | Message message = new Message(description, amount, added); 30 | return message; 31 | } 32 | static public Message CreateMessage(String description, BigDecimal amount){ 33 | return CreateMessage(description, amount, new GregorianCalendar()); 34 | } 35 | static public Message CreateMessage(BigDecimal amount){ 36 | return CreateMessage("", amount); 37 | } 38 | 39 | public String getDescription() { 40 | return description; 41 | } 42 | 43 | public BigDecimal getAmount() { 44 | return amount; 45 | } 46 | 47 | public Calendar getAdded() { 48 | return added; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/MessageConsumer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.math.BigDecimal; 10 | import java.math.BigInteger; 11 | import java.util.concurrent.BlockingQueue; 12 | import java.util.concurrent.Callable; 13 | import java.util.concurrent.CancellationException; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * 19 | * @author if 20 | */ 21 | public class MessageConsumer implements Callable{ 22 | final BlockingQueue q; 23 | private final AtomicInteger messagesReceived= new AtomicInteger(0); 24 | private final BigDecimal totalAmount = new BigDecimal(BigInteger.ZERO); 25 | 26 | private MessageConsumer(BlockingQueue q) { 27 | this.q = q; 28 | } 29 | public static MessageConsumer CreateMessageConsumer(BlockingQueue q){ 30 | return new MessageConsumer(q); 31 | } 32 | 33 | @Override 34 | public Integer call(){ 35 | try{ 36 | while(true){ 37 | Message message; 38 | message = q.take(); 39 | if(message==null) 40 | continue; 41 | 42 | messagesReceived.incrementAndGet(); 43 | synchronized(totalAmount){ 44 | totalAmount.add(message.getAmount()); 45 | } 46 | } 47 | }catch(InterruptedException e){ 48 | return getMessagesReceived(); 49 | } 50 | } 51 | 52 | public int getMessagesReceived() { 53 | return messagesReceived.get(); 54 | } 55 | 56 | public BigDecimal getTotalAmount() { 57 | synchronized(totalAmount){ 58 | return totalAmount; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/MessageConsumer4MBUS.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | import java.util.concurrent.Callable; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | /** 17 | * 18 | * @author if 19 | */ 20 | public class MessageConsumer4MBUS implements Callable{ 21 | 22 | private final AtomicInteger messagesReceived= new AtomicInteger(0); 23 | protected Integer incrementMessagesReceived() { 24 | return messagesReceived.incrementAndGet(); 25 | } 26 | 27 | Connection conn; 28 | PreparedStatement pollQueueSth; 29 | String qName; 30 | 31 | /** 32 | * 33 | * @param conn JDBC Connection 34 | * @param qName queue name 35 | * @return new MessageConsumer4MBUS 36 | * @throws SQLException 37 | */ 38 | public static MessageConsumer4MBUS CreateMessageConsumer4MBUS(Connection conn, String qName) throws SQLException { 39 | return new MessageConsumer4MBUS(conn, qName, conn.prepareStatement("select * from mbus.consume(?)")); 40 | } 41 | protected MessageConsumer4MBUS(Connection conn, String qName, PreparedStatement sth) throws SQLException { 42 | this.conn = conn; 43 | conn.setAutoCommit(false); 44 | this.qName = qName; 45 | this.pollQueueSth = sth; 46 | } 47 | 48 | @Override 49 | public Integer call() throws Exception{ 50 | int pollTriesCount=0; 51 | try{ 52 | while(true){ 53 | pollQueueSth.setString(1, qName); 54 | pollQueueSth.execute(); 55 | pollTriesCount++; 56 | ResultSet rs = pollQueueSth.getResultSet(); 57 | if(rs.next()){ 58 | incrementMessagesReceived(); 59 | rs.close(); 60 | if((pollTriesCount%10)==0) 61 | conn.commit(); 62 | continue; 63 | } 64 | conn.commit(); 65 | rs.close(); 66 | Thread.sleep(20); 67 | if(Thread.currentThread().isInterrupted()){ 68 | throw new InterruptedException("Interrupted"); 69 | } 70 | } 71 | }catch(InterruptedException e){ 72 | try{ 73 | pollQueueSth.close(); //conn.close(); 74 | } catch(SQLException e2){ 75 | throw new RuntimeException("Cannot close statement:"+e.getMessage(), e2); 76 | } 77 | return getMessagesReceived(); 78 | }catch(SQLException e){ 79 | try{ 80 | conn.rollback(); 81 | }catch(SQLException e2){ 82 | e2.initCause(e); 83 | throw new RuntimeException("Something goes wrong:"+e.getMessage(), e2); 84 | } 85 | System.out.println(e.getMessage()); 86 | throw new RuntimeException("SQL Exceptions:" + e.getMessage(), e); 87 | }finally{ 88 | try{ 89 | pollQueueSth.close(); 90 | conn.commit(); 91 | }catch(SQLException e){ 92 | throw new RuntimeException(e.getMessage(), e); 93 | } 94 | } 95 | } 96 | 97 | public int getMessagesReceived() { 98 | return messagesReceived.get(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/MessageConsumer4MBUSWithSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.sql.Connection; 10 | import java.sql.SQLException; 11 | 12 | /** 13 | * 14 | * @author if 15 | */ 16 | public class MessageConsumer4MBUSWithSelector extends MessageConsumer4MBUS{ 17 | final String subscriberName; 18 | public static MessageConsumer4MBUSWithSelector CreateMessageConsumer4MBUSWithSelector(Connection conn, String qName, String subscriber) throws SQLException { 19 | return new MessageConsumer4MBUSWithSelector(conn, qName, subscriber); 20 | } 21 | private MessageConsumer4MBUSWithSelector(Connection conn, String qName, String subscriber) throws SQLException { 22 | super(conn,qName,conn.prepareStatement("select * from mbus.consume(?,'"+ subscriber + "');")); 23 | subscriberName=subscriber; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/MessageProducer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.math.BigDecimal; 10 | import java.sql.Connection; 11 | import java.sql.SQLException; 12 | import java.util.concurrent.BlockingQueue; 13 | import java.util.concurrent.Callable; 14 | 15 | /** 16 | * 17 | * @author if 18 | */ 19 | public class MessageProducer implements Callable { 20 | final BlockingQueue q; 21 | final java.sql.Connection conn; 22 | private int messagesToSend; 23 | private int messagesSent=0; 24 | 25 | private MessageProducer(BlockingQueue q, Connection conn, int messagesToSend) { 26 | this.q = q; 27 | this.conn = conn; 28 | this.messagesToSend = messagesToSend; 29 | } 30 | 31 | public static MessageProducer CreateMessageProducer(BlockingQueue q, java.sql.Connection conn){ 32 | return new MessageProducer(q, conn, 100); 33 | } 34 | 35 | public static MessageProducer CreateMessageProducer(BlockingQueue q, java.sql.Connection conn, int messagesToSend){ 36 | return new MessageProducer(q, conn, messagesToSend); 37 | } 38 | 39 | @Override 40 | public Integer call(){ 41 | try{ 42 | while(messagesToSend>0){ 43 | if(Thread.currentThread().isInterrupted()) 44 | throw new InterruptedException("Got interrupt message"); 45 | Message message = Message.CreateMessage("Left "+(messagesToSend-1) + " messages", new BigDecimal(100)); 46 | q.put(message); 47 | messagesToSend--; 48 | messagesSent++; 49 | } 50 | conn.close(); 51 | }catch(InterruptedException e){ 52 | //do cleanup 53 | try{ 54 | conn.close(); 55 | }catch(SQLException ee){ 56 | throw new RuntimeException("Cannot close connection", ee); 57 | } 58 | Thread.currentThread().interrupted(); 59 | return messagesSent; 60 | }catch(Exception e){ 61 | throw new RuntimeException(e.getMessage(),e); 62 | } 63 | return messagesSent; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/MessageProducer4MBUS.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.SQLException; 12 | import java.sql.Statement; 13 | import java.util.concurrent.Callable; 14 | 15 | /** 16 | * 17 | * @author if 18 | * Makes a series of messages to specified mbus queue 19 | */ 20 | public class MessageProducer4MBUS implements Callable{ 21 | final protected Connection conn; 22 | final private String qName; 23 | final PreparedStatement sth; 24 | int messagesToSend; 25 | 26 | /** 27 | * Create a MBUS message producer. It generates series of a random messages 28 | * and post it into specified queue. 29 | * @param conn connection to database 30 | * @param qName queue name in the database 31 | * @param messagesToSend number of generated messages to send 32 | * @return created producer 33 | */ 34 | public static MessageProducer4MBUS CreateMessageProducer(Connection conn, String qName, int messagesToSend) throws SQLException { 35 | return new MessageProducer4MBUS(conn, qName, messagesToSend, conn.prepareStatement("select mbus.post(?,hstore(ARRAY[?,?,?,?]))")); 36 | } 37 | 38 | MessageProducer4MBUS(Connection conn, String qName, int messagesToSend, PreparedStatement sth) throws SQLException{ 39 | this.conn = conn; 40 | if(conn.getAutoCommit()) 41 | conn.setAutoCommit(false); 42 | this.qName = qName; 43 | this.messagesToSend = messagesToSend; 44 | this.sth = sth; 45 | } 46 | 47 | @Override 48 | public Integer call(){ 49 | int messagesSent=0; 50 | Exception exc = null; 51 | try{ 52 | while(messagesToSend>0){ 53 | if(Thread.currentThread().isInterrupted()) 54 | throw new InterruptedException(qName); 55 | 56 | int pIndex=1; 57 | sth.setString(pIndex++, qName); 58 | 59 | sth.setString(pIndex++, "data"); 60 | sth.setString(pIndex++, qName); 61 | 62 | sth.setString(pIndex++, "key"); 63 | sth.setString(pIndex++, java.lang.Integer.toString(messagesToSend)); 64 | 65 | sth.execute(); 66 | sth.getResultSet().close(); 67 | messagesSent++; 68 | messagesToSend--; 69 | if((messagesToSend%100)==0) 70 | conn.commit(); 71 | } 72 | conn.commit(); 73 | conn.close(); 74 | }catch(InterruptedException | SQLException ex){ 75 | Thread.currentThread().interrupt(); 76 | exc = ex; 77 | return 0; 78 | }finally{ 79 | try { 80 | if(Thread.currentThread().isInterrupted()){ 81 | conn.rollback(); 82 | //conn.close(); 83 | } 84 | } catch(SQLException e2){ 85 | e2.initCause(exc); 86 | throw new RuntimeException(e2); 87 | }; 88 | if(exc!=null) 89 | throw new RuntimeException("Cannot execute:" + exc.getMessage(), exc); 90 | } 91 | 92 | return messagesSent; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/ThreadFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * 13 | * @author if 14 | */ 15 | public class ThreadFactory implements java.util.concurrent.ThreadFactory { 16 | private List threads = new ArrayList(); 17 | private List runnables = new ArrayList<>(); 18 | 19 | @Override 20 | public Thread newThread(Runnable r) { 21 | Thread thread = new Thread(r); 22 | threads.add(thread); 23 | runnables.add(r); 24 | return thread; 25 | } 26 | 27 | public List getThreads() { 28 | return threads; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /trunk/tests/src/name/shaif/MBUSTest1/WrongCommandLineArgument.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | 7 | package name.shaif.MBUSTest1; 8 | 9 | /** 10 | * 11 | * @author if 12 | */ 13 | public class WrongCommandLineArgument extends Exception { 14 | 15 | ExceptionKind ek = ExceptionKind.FATAL; 16 | /** 17 | * Creates a new instance of WrongCommandLineArgument without 18 | * detail message. 19 | */ 20 | public WrongCommandLineArgument() { 21 | } 22 | 23 | /** 24 | * Constructs an instance of WrongCommandLineArgument with the 25 | * specified detail message. 26 | * 27 | * @param msg the detail message. 28 | */ 29 | public WrongCommandLineArgument(String msg) { 30 | super(msg); 31 | } 32 | public WrongCommandLineArgument(String msg, Throwable e) { 33 | super(msg,e); 34 | } 35 | } 36 | --------------------------------------------------------------------------------