[{"data":1,"prerenderedAt":2002},["ShallowReactive",2],{"article_list_postgresql_":3},[4],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":10,"tags":11,"excerpt":10,"image":15,"publishDate":16,"body":17,"_type":1994,"_id":1995,"_source":1407,"_file":1996,"_stem":1997,"_extension":1998,"author":1999},"/dpopowich/2023-8/postgres-pubsub","2023-8",false,"","Using PostgreSQL for Pub/Sub","A+L has been working on a Single Page Application (SPA) wherein our client's users take on the role of Staff Users (think: project managers) as they aid their Customer Users in using the application to complete a complex project.",[12,13,14],"postgresql","python","async","/dpopowich/2023-8/img/psql_pub_sub.png","2024-04-15",{"type":18,"children":19,"toc":1980},"root",[20,53,58,67,167,179,184,192,211,232,237,261,266,272,294,299,304,310,315,324,372,408,474,490,679,696,702,707,758,770,817,837,868,886,909,921,958,970,995,1116,1121,1143,1178,1189,1206,1211,1462,1467,1573,1578,1584,1605,1616,1647,1653,1680,1711,1737,1814,1819,1890,1896,1921,1933,1945,1974],{"type":21,"tag":22,"props":23,"children":24},"element","p",{},[25,28,35,37,43,45,51],{"type":26,"value":27},"text","A+L has been working on a Single Page Application (SPA) wherein our client's users take on the role of ",{"type":21,"tag":29,"props":30,"children":32},"code",{"className":31},[],[33],{"type":26,"value":34},"Staff Users",{"type":26,"value":36}," (think: project managers) as they aid ",{"type":21,"tag":38,"props":39,"children":40},"em",{},[41],{"type":26,"value":42},"their",{"type":26,"value":44}," ",{"type":21,"tag":29,"props":46,"children":48},{"className":47},[],[49],{"type":26,"value":50},"Customer Users",{"type":26,"value":52}," in using the application to complete a complex project.",{"type":21,"tag":22,"props":54,"children":55},{},[56],{"type":26,"value":57},"The architecture is common to many SPAs:",{"type":21,"tag":22,"props":59,"children":60},{},[61],{"type":21,"tag":62,"props":63,"children":66},"img",{"alt":64,"src":65},"architecture","/dpopowich/2023-8/spa.png",[],{"type":21,"tag":68,"props":69,"children":70},"ul",{},[71,86,118,130,162],{"type":21,"tag":72,"props":73,"children":74},"li",{},[75,77],{"type":26,"value":76},"RDBMS - ",{"type":21,"tag":78,"props":79,"children":83},"a",{"href":80,"rel":81},"https://www.postgresql.org/",[82],"nofollow",[84],{"type":26,"value":85},"PostgreSQL",{"type":21,"tag":72,"props":87,"children":88},{},[89,91,98,100,107,109,116],{"type":26,"value":90},"REST API Server - ",{"type":21,"tag":78,"props":92,"children":95},{"href":93,"rel":94},"https://docs.python.org/3.11/library/asyncio.html#module-asyncio",[82],[96],{"type":26,"value":97},"asynchronous python-3.11",{"type":26,"value":99}," using ",{"type":21,"tag":78,"props":101,"children":104},{"href":102,"rel":103},"https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html",[82],[105],{"type":26,"value":106},"SQLAlchemy 2.x",{"type":26,"value":108}," with ",{"type":21,"tag":78,"props":110,"children":113},{"href":111,"rel":112},"https://magicstack.github.io/asyncpg/current/",[82],[114],{"type":26,"value":115},"asyncpg",{"type":26,"value":117}," as the database engine.",{"type":21,"tag":72,"props":119,"children":120},{},[121,123],{"type":26,"value":122},"Reverse Proxy Server - ",{"type":21,"tag":78,"props":124,"children":127},{"href":125,"rel":126},"https://www.nginx.com/",[82],[128],{"type":26,"value":129},"nginx",{"type":21,"tag":72,"props":131,"children":132},{},[133,135,142,144,148,150,154,156,160],{"type":26,"value":134},"SPA Clients - ",{"type":21,"tag":78,"props":136,"children":139},{"href":137,"rel":138},"https://vuejs.org/",[82],[140],{"type":26,"value":141},"Vue",{"type":26,"value":143},".  As noted: the SPA supports different classes of users.  Our client's users are the ",{"type":21,"tag":38,"props":145,"children":146},{},[147],{"type":26,"value":34},{"type":26,"value":149},", taking on the role of Project Manager, aiding ",{"type":21,"tag":38,"props":151,"children":152},{},[153],{"type":26,"value":42},{"type":26,"value":155}," customers (",{"type":21,"tag":38,"props":157,"children":158},{},[159],{"type":26,"value":50},{"type":26,"value":161}," in the diagram) in completing a project.",{"type":21,"tag":72,"props":163,"children":164},{},[165],{"type":26,"value":166},"Notifications - early versions of the application configured the API servers to send notifications via an external SMTP server.",{"type":21,"tag":168,"props":169,"children":171},"h2",{"id":170},"live-updates",[172,177],{"type":21,"tag":38,"props":173,"children":174},{},[175],{"type":26,"value":176},"Live",{"type":26,"value":178}," Updates",{"type":21,"tag":22,"props":180,"children":181},{},[182],{"type":26,"value":183},"The SPA has a dashboard which shows a Customer User their progress in completing all their tasks; a simple graphic of a bar, 0% to 100%, with the bar filled to the percentage of tasks completed by the customer.",{"type":21,"tag":22,"props":185,"children":186},{},[187],{"type":21,"tag":62,"props":188,"children":191},{"alt":189,"src":190},"progress","/dpopowich/2023-8/img/progress.png",[],{"type":21,"tag":22,"props":193,"children":194},{},[195,197,202,204,209],{"type":26,"value":196},"Below the graphic, a list of outstanding tasks is displayed.  An interesting aspect of this application is that many customers, working on the same project, can affect ",{"type":21,"tag":38,"props":198,"children":199},{},[200],{"type":26,"value":201},"other",{"type":26,"value":203}," customer's progress.  E.g., a customer who is idle in the application may still see the progress increase as ",{"type":21,"tag":38,"props":205,"children":206},{},[207],{"type":26,"value":208},"another",{"type":26,"value":210}," customer completes a task.",{"type":21,"tag":22,"props":212,"children":213},{},[214,216,223,225,230],{"type":26,"value":215},"Early demands on the project (mostly time-demands on getting a ",{"type":21,"tag":78,"props":217,"children":220},{"href":218,"rel":219},"https://en.wikipedia.org/wiki/Minimum_viable_product",[82],[221],{"type":26,"value":222},"MVP",{"type":26,"value":224}," delivered for client review) prevented us from integrating websockets, so the SPA polls every ",{"type":21,"tag":38,"props":226,"children":227},{},[228],{"type":26,"value":229},"n",{"type":26,"value":231},"-minutes for progress via the REST API.",{"type":21,"tag":22,"props":233,"children":234},{},[235],{"type":26,"value":236},"As we approach publication of the first version of the product we're now exploring ideas we had to table during early MVP development.  These future enhancements mostly center around data updates and notifications:",{"type":21,"tag":68,"props":238,"children":239},{},[240,245,250],{"type":21,"tag":72,"props":241,"children":242},{},[243],{"type":26,"value":244},"Calculating progress is not cheap. Having every user poll every n-minutes is very expensive, especially when you consider it is difficult for the SPA to know how frequent to poll.  Since the server knows when data comes in from collaborating users that may affect progress, it's a clear win to have the server send notifications.",{"type":21,"tag":72,"props":246,"children":247},{},[248],{"type":26,"value":249},"There's a fair amount of complexity in completing tasks and it is expected customers will have several questions for staff, so there's a desire to add a live chat feature, allowing customers to ask questions.  These questions and answers are not ephemeral, but must be recorded for the life of the project.",{"type":21,"tag":72,"props":251,"children":252},{},[253,255,259],{"type":26,"value":254},"We're planning on removing immediate notification of events by email, rather, sending notifications via the same mechanism as live chat.  To handle missed notifications, a background task can poll the database for unseen notifications and send email only after ",{"type":21,"tag":38,"props":256,"children":257},{},[258],{"type":26,"value":229},{"type":26,"value":260},"-hours of going unread.  This will also limit \"spamming\" staff with each completed task (which they might already know about if they're active in the SPA), spooling up several notifications in one email.",{"type":21,"tag":22,"props":262,"children":263},{},[264],{"type":26,"value":265},"Enter websockets.  Integrating websockets gives the server a direct connection to the SPA and can send notifications for all these desired notifications.",{"type":21,"tag":168,"props":267,"children":269},{"id":268},"pubsub",[270],{"type":26,"value":271},"Pub/Sub",{"type":21,"tag":22,"props":273,"children":274},{},[275,277,283,285,292],{"type":26,"value":276},"When I think of implementing ",{"type":21,"tag":78,"props":278,"children":281},{"href":279,"rel":280},"https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern",[82],[282],{"type":26,"value":271},{"type":26,"value":284}," for an application my go-to has been ",{"type":21,"tag":78,"props":286,"children":289},{"href":287,"rel":288},"https://redis.io/docs/interact/pubsub/",[82],[290],{"type":26,"value":291},"redis",{"type":26,"value":293},".  It is both performant and scalable.  It is an excellent choice for many use-cases and particularly useful for ephemeral communication that is not tied to any data.",{"type":21,"tag":22,"props":295,"children":296},{},[297],{"type":26,"value":298},"But if you need to know your notifications have been seen or need archives of all communication, you will need persistence of your messages.  This makes redis less desireable as you will need to maintain your data in two places: 1) a permanent storage, like a RDBMS, 2) your redis server.",{"type":21,"tag":22,"props":300,"children":301},{},[302],{"type":26,"value":303},"If you're using PostgreSQL for your data storage you already have (a little known) pub/sub engine.",{"type":21,"tag":168,"props":305,"children":307},{"id":306},"demo-preparation",[308],{"type":26,"value":309},"Demo Preparation",{"type":21,"tag":22,"props":311,"children":312},{},[313],{"type":26,"value":314},"You can run all the code in this tutorial if you have python-3 (tested on 3.8–3.11) and access to docker for installation of PostgreSQL.  If you have earlier versions of python and/or your own installation of PostgreSQL, your mileage may vary as you follow along.",{"type":21,"tag":316,"props":317,"children":318},"ol",{},[319],{"type":21,"tag":72,"props":320,"children":321},{},[322],{"type":26,"value":323},"Clone the repo holding demo SQL and python script:",{"type":21,"tag":325,"props":326,"children":330},"pre",{"className":327,"code":328,"language":329,"meta":8,"style":8},"language-console shiki shiki-themes github-light github-dark","$ git clone https://github.com/artandlogic/postgresql-pubsub-demo.git\n$ cd postgresql-pubsub-demo\n$ ls\ndemo.py  demo.sql  README.md\n","console",[331],{"type":21,"tag":29,"props":332,"children":333},{"__ignoreMap":8},[334,345,354,363],{"type":21,"tag":335,"props":336,"children":339},"span",{"class":337,"line":338},"line",1,[340],{"type":21,"tag":335,"props":341,"children":342},{},[343],{"type":26,"value":344},"$ git clone https://github.com/artandlogic/postgresql-pubsub-demo.git\n",{"type":21,"tag":335,"props":346,"children":348},{"class":337,"line":347},2,[349],{"type":21,"tag":335,"props":350,"children":351},{},[352],{"type":26,"value":353},"$ cd postgresql-pubsub-demo\n",{"type":21,"tag":335,"props":355,"children":357},{"class":337,"line":356},3,[358],{"type":21,"tag":335,"props":359,"children":360},{},[361],{"type":26,"value":362},"$ ls\n",{"type":21,"tag":335,"props":364,"children":366},{"class":337,"line":365},4,[367],{"type":21,"tag":335,"props":368,"children":369},{},[370],{"type":26,"value":371},"demo.py  demo.sql  README.md\n",{"type":21,"tag":316,"props":373,"children":374},{"start":347},[375,396],{"type":21,"tag":72,"props":376,"children":377},{},[378,380,386,388,394],{"type":26,"value":379},"Review two files in the repo: ",{"type":21,"tag":29,"props":381,"children":383},{"className":382},[],[384],{"type":26,"value":385},"demo.sql",{"type":26,"value":387},", SQL schema for the demo, and ",{"type":21,"tag":29,"props":389,"children":391},{"className":390},[],[392],{"type":26,"value":393},"demo.py",{"type":26,"value":395},", a simple asynchronous python application.",{"type":21,"tag":72,"props":397,"children":398},{},[399,401,406],{"type":26,"value":400},"Start PostgreSQL via docker.  The following commands will launch a container, loading the SQL downloaded in step 1.  It is important to run the command from the cloned directory containing ",{"type":21,"tag":29,"props":402,"children":404},{"className":403},[],[405],{"type":26,"value":385},{"type":26,"value":407},", so the database initializes properly.  This binds host port 8432 to the container's port 5432 (postgresql).  If that port is not available, edit as necessary",{"type":21,"tag":325,"props":409,"children":411},{"className":327,"code":410,"language":329,"meta":8,"style":8},"$ export PGHOST=localhost PGPORT=8432 PGUSER=demo PGPASSWORD=\"passw0rd!\"\n$ docker run --rm -d --name pg-pubsub-demo  \\\n    -v $(pwd):/docker-entrypoint-initdb.d \\\n    -e POSTGRES_USER=$PGUSER -e POSTGRES_PASSWORD=$PGPASSWORD \\\n    -p $PGPORT:5432 \\\n    postgres:14-alpine\n$ alias psql='docker exec -it pg-pubsub-demo psql -U demo'\n",[412],{"type":21,"tag":29,"props":413,"children":414},{"__ignoreMap":8},[415,423,431,439,447,456,465],{"type":21,"tag":335,"props":416,"children":417},{"class":337,"line":338},[418],{"type":21,"tag":335,"props":419,"children":420},{},[421],{"type":26,"value":422},"$ export PGHOST=localhost PGPORT=8432 PGUSER=demo PGPASSWORD=\"passw0rd!\"\n",{"type":21,"tag":335,"props":424,"children":425},{"class":337,"line":347},[426],{"type":21,"tag":335,"props":427,"children":428},{},[429],{"type":26,"value":430},"$ docker run --rm -d --name pg-pubsub-demo  \\\n",{"type":21,"tag":335,"props":432,"children":433},{"class":337,"line":356},[434],{"type":21,"tag":335,"props":435,"children":436},{},[437],{"type":26,"value":438},"    -v $(pwd):/docker-entrypoint-initdb.d \\\n",{"type":21,"tag":335,"props":440,"children":441},{"class":337,"line":365},[442],{"type":21,"tag":335,"props":443,"children":444},{},[445],{"type":26,"value":446},"    -e POSTGRES_USER=$PGUSER -e POSTGRES_PASSWORD=$PGPASSWORD \\\n",{"type":21,"tag":335,"props":448,"children":450},{"class":337,"line":449},5,[451],{"type":21,"tag":335,"props":452,"children":453},{},[454],{"type":26,"value":455},"    -p $PGPORT:5432 \\\n",{"type":21,"tag":335,"props":457,"children":459},{"class":337,"line":458},6,[460],{"type":21,"tag":335,"props":461,"children":462},{},[463],{"type":26,"value":464},"    postgres:14-alpine\n",{"type":21,"tag":335,"props":466,"children":468},{"class":337,"line":467},7,[469],{"type":21,"tag":335,"props":470,"children":471},{},[472],{"type":26,"value":473},"$ alias psql='docker exec -it pg-pubsub-demo psql -U demo'\n",{"type":21,"tag":316,"props":475,"children":476},{"start":365},[477],{"type":21,"tag":72,"props":478,"children":479},{},[480,482,488],{"type":26,"value":481},"Run ",{"type":21,"tag":29,"props":483,"children":485},{"className":484},[],[486],{"type":26,"value":487},"psql",{"type":26,"value":489}," to verify the setup.  You should see the following:",{"type":21,"tag":325,"props":491,"children":493},{"className":327,"code":492,"language":329,"meta":8,"style":8},"$ psql\npsql (14.5)\nType \"help\" for help.\n\ndemo=# \\d\n             List of relations\n Schema |      Name      |   Type   | Owner\n--------+----------------+----------+-------\n public | appuser        | table    | demo\n public | appuser_id_seq | sequence | demo\n public | message        | table    | demo\n public | message_id_seq | sequence | demo\n(4 rows)\n\ndemo=# select * from appuser;\n id | fullname\n----+----------\n  1 | Abe\n  2 | Bob\n  3 | Charlie\n(3 rows)\n",[494],{"type":21,"tag":29,"props":495,"children":496},{"__ignoreMap":8},[497,505,513,521,530,538,546,554,563,572,581,590,599,608,616,625,634,643,652,661,670],{"type":21,"tag":335,"props":498,"children":499},{"class":337,"line":338},[500],{"type":21,"tag":335,"props":501,"children":502},{},[503],{"type":26,"value":504},"$ psql\n",{"type":21,"tag":335,"props":506,"children":507},{"class":337,"line":347},[508],{"type":21,"tag":335,"props":509,"children":510},{},[511],{"type":26,"value":512},"psql (14.5)\n",{"type":21,"tag":335,"props":514,"children":515},{"class":337,"line":356},[516],{"type":21,"tag":335,"props":517,"children":518},{},[519],{"type":26,"value":520},"Type \"help\" for help.\n",{"type":21,"tag":335,"props":522,"children":523},{"class":337,"line":365},[524],{"type":21,"tag":335,"props":525,"children":527},{"emptyLinePlaceholder":526},true,[528],{"type":26,"value":529},"\n",{"type":21,"tag":335,"props":531,"children":532},{"class":337,"line":449},[533],{"type":21,"tag":335,"props":534,"children":535},{},[536],{"type":26,"value":537},"demo=# \\d\n",{"type":21,"tag":335,"props":539,"children":540},{"class":337,"line":458},[541],{"type":21,"tag":335,"props":542,"children":543},{},[544],{"type":26,"value":545},"             List of relations\n",{"type":21,"tag":335,"props":547,"children":548},{"class":337,"line":467},[549],{"type":21,"tag":335,"props":550,"children":551},{},[552],{"type":26,"value":553}," Schema |      Name      |   Type   | Owner\n",{"type":21,"tag":335,"props":555,"children":557},{"class":337,"line":556},8,[558],{"type":21,"tag":335,"props":559,"children":560},{},[561],{"type":26,"value":562},"--------+----------------+----------+-------\n",{"type":21,"tag":335,"props":564,"children":566},{"class":337,"line":565},9,[567],{"type":21,"tag":335,"props":568,"children":569},{},[570],{"type":26,"value":571}," public | appuser        | table    | demo\n",{"type":21,"tag":335,"props":573,"children":575},{"class":337,"line":574},10,[576],{"type":21,"tag":335,"props":577,"children":578},{},[579],{"type":26,"value":580}," public | appuser_id_seq | sequence | demo\n",{"type":21,"tag":335,"props":582,"children":584},{"class":337,"line":583},11,[585],{"type":21,"tag":335,"props":586,"children":587},{},[588],{"type":26,"value":589}," public | message        | table    | demo\n",{"type":21,"tag":335,"props":591,"children":593},{"class":337,"line":592},12,[594],{"type":21,"tag":335,"props":595,"children":596},{},[597],{"type":26,"value":598}," public | message_id_seq | sequence | demo\n",{"type":21,"tag":335,"props":600,"children":602},{"class":337,"line":601},13,[603],{"type":21,"tag":335,"props":604,"children":605},{},[606],{"type":26,"value":607},"(4 rows)\n",{"type":21,"tag":335,"props":609,"children":611},{"class":337,"line":610},14,[612],{"type":21,"tag":335,"props":613,"children":614},{"emptyLinePlaceholder":526},[615],{"type":26,"value":529},{"type":21,"tag":335,"props":617,"children":619},{"class":337,"line":618},15,[620],{"type":21,"tag":335,"props":621,"children":622},{},[623],{"type":26,"value":624},"demo=# select * from appuser;\n",{"type":21,"tag":335,"props":626,"children":628},{"class":337,"line":627},16,[629],{"type":21,"tag":335,"props":630,"children":631},{},[632],{"type":26,"value":633}," id | fullname\n",{"type":21,"tag":335,"props":635,"children":637},{"class":337,"line":636},17,[638],{"type":21,"tag":335,"props":639,"children":640},{},[641],{"type":26,"value":642},"----+----------\n",{"type":21,"tag":335,"props":644,"children":646},{"class":337,"line":645},18,[647],{"type":21,"tag":335,"props":648,"children":649},{},[650],{"type":26,"value":651},"  1 | Abe\n",{"type":21,"tag":335,"props":653,"children":655},{"class":337,"line":654},19,[656],{"type":21,"tag":335,"props":657,"children":658},{},[659],{"type":26,"value":660},"  2 | Bob\n",{"type":21,"tag":335,"props":662,"children":664},{"class":337,"line":663},20,[665],{"type":21,"tag":335,"props":666,"children":667},{},[668],{"type":26,"value":669},"  3 | Charlie\n",{"type":21,"tag":335,"props":671,"children":673},{"class":337,"line":672},21,[674],{"type":21,"tag":335,"props":675,"children":676},{},[677],{"type":26,"value":678},"(3 rows)\n",{"type":21,"tag":680,"props":681,"children":682},"blockquote",{},[683],{"type":21,"tag":22,"props":684,"children":685},{},[686,688,694],{"type":26,"value":687},"NOTE: When you are finished with the demo, you can stop/remove the container with ",{"type":21,"tag":29,"props":689,"children":691},{"className":690},[],[692],{"type":26,"value":693},"docker stop pg-pubsub-demo",{"type":26,"value":695},".",{"type":21,"tag":168,"props":697,"children":699},{"id":698},"notifylistenunlisten",[700],{"type":26,"value":701},"NOTIFY/LISTEN/UNLISTEN",{"type":21,"tag":22,"props":703,"children":704},{},[705],{"type":26,"value":706},"PostgreSQL has three non-standard SQL commands:",{"type":21,"tag":68,"props":708,"children":709},{},[710,726,742],{"type":21,"tag":72,"props":711,"children":712},{},[713,724],{"type":21,"tag":78,"props":714,"children":717},{"href":715,"rel":716},"https://www.postgresql.org/docs/14/sql-notify.html",[82],[718],{"type":21,"tag":29,"props":719,"children":721},{"className":720},[],[722],{"type":26,"value":723},"NOTIFY",{"type":26,"value":725}," - publish to a channel.",{"type":21,"tag":72,"props":727,"children":728},{},[729,740],{"type":21,"tag":78,"props":730,"children":733},{"href":731,"rel":732},"https://www.postgresql.org/docs/14/sql-listen.html",[82],[734],{"type":21,"tag":29,"props":735,"children":737},{"className":736},[],[738],{"type":26,"value":739},"LISTEN",{"type":26,"value":741}," - subscribe to a channel.",{"type":21,"tag":72,"props":743,"children":744},{},[745,756],{"type":21,"tag":78,"props":746,"children":749},{"href":747,"rel":748},"https://www.postgresql.org/docs/14/sql-unlisten.html",[82],[750],{"type":21,"tag":29,"props":751,"children":753},{"className":752},[],[754],{"type":26,"value":755},"UNLISTEN",{"type":26,"value":757}," - unsubscribe from a channel",{"type":21,"tag":22,"props":759,"children":760},{},[761,763,768],{"type":26,"value":762},"Let's look at examples with ",{"type":21,"tag":29,"props":764,"children":766},{"className":765},[],[767],{"type":26,"value":487},{"type":26,"value":769},":",{"type":21,"tag":325,"props":771,"children":773},{"className":327,"code":772,"language":329,"meta":8,"style":8},"demo=# listen foo;\nLISTEN\ndemo=# notify foo, 'Hello, world';\nNOTIFY\nAsynchronous notification \"foo\" with payload \"Hello, world\" received from server process with PID 60807.\n",[774],{"type":21,"tag":29,"props":775,"children":776},{"__ignoreMap":8},[777,785,793,801,809],{"type":21,"tag":335,"props":778,"children":779},{"class":337,"line":338},[780],{"type":21,"tag":335,"props":781,"children":782},{},[783],{"type":26,"value":784},"demo=# listen foo;\n",{"type":21,"tag":335,"props":786,"children":787},{"class":337,"line":347},[788],{"type":21,"tag":335,"props":789,"children":790},{},[791],{"type":26,"value":792},"LISTEN\n",{"type":21,"tag":335,"props":794,"children":795},{"class":337,"line":356},[796],{"type":21,"tag":335,"props":797,"children":798},{},[799],{"type":26,"value":800},"demo=# notify foo, 'Hello, world';\n",{"type":21,"tag":335,"props":802,"children":803},{"class":337,"line":365},[804],{"type":21,"tag":335,"props":805,"children":806},{},[807],{"type":26,"value":808},"NOTIFY\n",{"type":21,"tag":335,"props":810,"children":811},{"class":337,"line":449},[812],{"type":21,"tag":335,"props":813,"children":814},{},[815],{"type":26,"value":816},"Asynchronous notification \"foo\" with payload \"Hello, world\" received from server process with PID 60807.\n",{"type":21,"tag":22,"props":818,"children":819},{},[820,822,827,829,835],{"type":26,"value":821},"The ",{"type":21,"tag":29,"props":823,"children":825},{"className":824},[],[826],{"type":26,"value":723},{"type":26,"value":828}," command expects constants for both the channel name (a legal SQL identifier) and the text notification (type ",{"type":21,"tag":29,"props":830,"children":832},{"className":831},[],[833],{"type":26,"value":834},"TEXT",{"type":26,"value":836},").  You cannot use an expression:",{"type":21,"tag":325,"props":838,"children":840},{"className":327,"code":839,"language":329,"meta":8,"style":8},"demo=# notify foo, 'This will ' || 'not work.';\nERROR:  syntax error at or near \"||\"\nLINE 1: notify foo, 'This will ' || 'not work.';\n",[841],{"type":21,"tag":29,"props":842,"children":843},{"__ignoreMap":8},[844,852,860],{"type":21,"tag":335,"props":845,"children":846},{"class":337,"line":338},[847],{"type":21,"tag":335,"props":848,"children":849},{},[850],{"type":26,"value":851},"demo=# notify foo, 'This will ' || 'not work.';\n",{"type":21,"tag":335,"props":853,"children":854},{"class":337,"line":347},[855],{"type":21,"tag":335,"props":856,"children":857},{},[858],{"type":26,"value":859},"ERROR:  syntax error at or near \"||\"\n",{"type":21,"tag":335,"props":861,"children":862},{"class":337,"line":356},[863],{"type":21,"tag":335,"props":864,"children":865},{},[866],{"type":26,"value":867},"LINE 1: notify foo, 'This will ' || 'not work.';\n",{"type":21,"tag":22,"props":869,"children":870},{},[871,873,884],{"type":26,"value":872},"Fortunately, PostgreSQL provides the function ",{"type":21,"tag":78,"props":874,"children":877},{"href":875,"rel":876},"https://www.postgresql.org/docs/14/sql-notify.html#id-1.9.3.158.7.5",[82],[878],{"type":21,"tag":29,"props":879,"children":881},{"className":880},[],[882],{"type":26,"value":883},"pg_notify",{"type":26,"value":885}," which allows dynamic generation of the notifications:",{"type":21,"tag":325,"props":887,"children":889},{"className":327,"code":888,"language":329,"meta":8,"style":8},"demo=# select pg_notify('foo', 'This ' || 'is a ' || 'computed value!');\nAsynchronous notification \"foo\" with payload \"This is a computed value!\" received from server process with PID 60807.\n",[890],{"type":21,"tag":29,"props":891,"children":892},{"__ignoreMap":8},[893,901],{"type":21,"tag":335,"props":894,"children":895},{"class":337,"line":338},[896],{"type":21,"tag":335,"props":897,"children":898},{},[899],{"type":26,"value":900},"demo=# select pg_notify('foo', 'This ' || 'is a ' || 'computed value!');\n",{"type":21,"tag":335,"props":902,"children":903},{"class":337,"line":347},[904],{"type":21,"tag":335,"props":905,"children":906},{},[907],{"type":26,"value":908},"Asynchronous notification \"foo\" with payload \"This is a computed value!\" received from server process with PID 60807.\n",{"type":21,"tag":22,"props":910,"children":911},{},[912,914,919],{"type":26,"value":913},"A session can \"unsubscibe\" with the ",{"type":21,"tag":29,"props":915,"children":917},{"className":916},[],[918],{"type":26,"value":755},{"type":26,"value":920}," command:",{"type":21,"tag":325,"props":922,"children":924},{"className":327,"code":923,"language":329,"meta":8,"style":8},"demo=# unlisten foo;\nUNLISTEN\ndemo=# notify foo, 'Hello, world';\nNOTIFY\n",[925],{"type":21,"tag":29,"props":926,"children":927},{"__ignoreMap":8},[928,936,944,951],{"type":21,"tag":335,"props":929,"children":930},{"class":337,"line":338},[931],{"type":21,"tag":335,"props":932,"children":933},{},[934],{"type":26,"value":935},"demo=# unlisten foo;\n",{"type":21,"tag":335,"props":937,"children":938},{"class":337,"line":347},[939],{"type":21,"tag":335,"props":940,"children":941},{},[942],{"type":26,"value":943},"UNLISTEN\n",{"type":21,"tag":335,"props":945,"children":946},{"class":337,"line":356},[947],{"type":21,"tag":335,"props":948,"children":949},{},[950],{"type":26,"value":800},{"type":21,"tag":335,"props":952,"children":953},{"class":337,"line":365},[954],{"type":21,"tag":335,"props":955,"children":956},{},[957],{"type":26,"value":808},{"type":21,"tag":959,"props":960,"children":962},"h3",{"id":961},"notify-in-transactions",[963,968],{"type":21,"tag":29,"props":964,"children":966},{"className":965},[],[967],{"type":26,"value":723},{"type":26,"value":969}," in Transactions",{"type":21,"tag":22,"props":971,"children":972},{},[973,975,980,982,987,989,994],{"type":26,"value":974},"What is particularly attractive about ",{"type":21,"tag":29,"props":976,"children":978},{"className":977},[],[979],{"type":26,"value":723},{"type":26,"value":981}," is you can place it in a transaction and if the transaction fails, ",{"type":21,"tag":38,"props":983,"children":984},{},[985],{"type":26,"value":986},"notifications are not delivered",{"type":26,"value":988},".  Back in ",{"type":21,"tag":29,"props":990,"children":992},{"className":991},[],[993],{"type":26,"value":487},{"type":26,"value":769},{"type":21,"tag":325,"props":996,"children":998},{"className":327,"code":997,"language":329,"meta":8,"style":8},"demo=# listen foo;\nLISTEN\ndemo=# begin;\nBEGIN\ndemo=*# notify foo, 'This will not be delivered if we rollback!';\nNOTIFY\ndemo=*# rollback;\nROLLBACK\ndemo=# begin;\nBEGIN\ndemo=*# notify foo, 'This will be delivered when we commit!';\nNOTIFY\ndemo=*# commit;\nCOMMIT\nAsynchronous notification \"foo\" with payload \"This will be delivered when we commit!\" received from server process with PID 60807.\n",[999],{"type":21,"tag":29,"props":1000,"children":1001},{"__ignoreMap":8},[1002,1009,1016,1024,1032,1040,1047,1055,1063,1070,1077,1085,1092,1100,1108],{"type":21,"tag":335,"props":1003,"children":1004},{"class":337,"line":338},[1005],{"type":21,"tag":335,"props":1006,"children":1007},{},[1008],{"type":26,"value":784},{"type":21,"tag":335,"props":1010,"children":1011},{"class":337,"line":347},[1012],{"type":21,"tag":335,"props":1013,"children":1014},{},[1015],{"type":26,"value":792},{"type":21,"tag":335,"props":1017,"children":1018},{"class":337,"line":356},[1019],{"type":21,"tag":335,"props":1020,"children":1021},{},[1022],{"type":26,"value":1023},"demo=# begin;\n",{"type":21,"tag":335,"props":1025,"children":1026},{"class":337,"line":365},[1027],{"type":21,"tag":335,"props":1028,"children":1029},{},[1030],{"type":26,"value":1031},"BEGIN\n",{"type":21,"tag":335,"props":1033,"children":1034},{"class":337,"line":449},[1035],{"type":21,"tag":335,"props":1036,"children":1037},{},[1038],{"type":26,"value":1039},"demo=*# notify foo, 'This will not be delivered if we rollback!';\n",{"type":21,"tag":335,"props":1041,"children":1042},{"class":337,"line":458},[1043],{"type":21,"tag":335,"props":1044,"children":1045},{},[1046],{"type":26,"value":808},{"type":21,"tag":335,"props":1048,"children":1049},{"class":337,"line":467},[1050],{"type":21,"tag":335,"props":1051,"children":1052},{},[1053],{"type":26,"value":1054},"demo=*# rollback;\n",{"type":21,"tag":335,"props":1056,"children":1057},{"class":337,"line":556},[1058],{"type":21,"tag":335,"props":1059,"children":1060},{},[1061],{"type":26,"value":1062},"ROLLBACK\n",{"type":21,"tag":335,"props":1064,"children":1065},{"class":337,"line":565},[1066],{"type":21,"tag":335,"props":1067,"children":1068},{},[1069],{"type":26,"value":1023},{"type":21,"tag":335,"props":1071,"children":1072},{"class":337,"line":574},[1073],{"type":21,"tag":335,"props":1074,"children":1075},{},[1076],{"type":26,"value":1031},{"type":21,"tag":335,"props":1078,"children":1079},{"class":337,"line":583},[1080],{"type":21,"tag":335,"props":1081,"children":1082},{},[1083],{"type":26,"value":1084},"demo=*# notify foo, 'This will be delivered when we commit!';\n",{"type":21,"tag":335,"props":1086,"children":1087},{"class":337,"line":592},[1088],{"type":21,"tag":335,"props":1089,"children":1090},{},[1091],{"type":26,"value":808},{"type":21,"tag":335,"props":1093,"children":1094},{"class":337,"line":601},[1095],{"type":21,"tag":335,"props":1096,"children":1097},{},[1098],{"type":26,"value":1099},"demo=*# commit;\n",{"type":21,"tag":335,"props":1101,"children":1102},{"class":337,"line":610},[1103],{"type":21,"tag":335,"props":1104,"children":1105},{},[1106],{"type":26,"value":1107},"COMMIT\n",{"type":21,"tag":335,"props":1109,"children":1110},{"class":337,"line":618},[1111],{"type":21,"tag":335,"props":1112,"children":1113},{},[1114],{"type":26,"value":1115},"Asynchronous notification \"foo\" with payload \"This will be delivered when we commit!\" received from server process with PID 60807.\n",{"type":21,"tag":22,"props":1117,"children":1118},{},[1119],{"type":26,"value":1120},"This means we can write trigger functions that send notifications on events, making our notifications data-driven.",{"type":21,"tag":680,"props":1122,"children":1123},{},[1124],{"type":21,"tag":22,"props":1125,"children":1126},{},[1127,1129,1134,1136,1141],{"type":26,"value":1128},"NOTE: Compare and contrast with what we would need to do if we used redis for pub/sub.  After committing out postgres transaction we would have to make a separate call to redis.  This separation of data ",{"type":21,"tag":38,"props":1130,"children":1131},{},[1132],{"type":26,"value":1133},"event",{"type":26,"value":1135}," from event ",{"type":21,"tag":38,"props":1137,"children":1138},{},[1139],{"type":26,"value":1140},"notification",{"type":26,"value":1142}," would be a weak link in the system.",{"type":21,"tag":22,"props":1144,"children":1145},{},[1146,1148,1153,1155,1161,1163,1169,1171,1177],{"type":26,"value":1147},"If you look at ",{"type":21,"tag":29,"props":1149,"children":1151},{"className":1150},[],[1152],{"type":26,"value":385},{"type":26,"value":1154}," you will see a table, ",{"type":21,"tag":29,"props":1156,"children":1158},{"className":1157},[],[1159],{"type":26,"value":1160},"message",{"type":26,"value":1162},", for the permanent storage of all messages, and a trigger function, ",{"type":21,"tag":29,"props":1164,"children":1166},{"className":1165},[],[1167],{"type":26,"value":1168},"notify_message()",{"type":26,"value":1170},", that will send a notification (the JSON representation of the inserted record) on successful ",{"type":21,"tag":29,"props":1172,"children":1174},{"className":1173},[],[1175],{"type":26,"value":1176},"INSERT",{"type":26,"value":695},{"type":21,"tag":22,"props":1179,"children":1180},{},[1181,1183,1188],{"type":26,"value":1182},"In ",{"type":21,"tag":29,"props":1184,"children":1186},{"className":1185},[],[1187],{"type":26,"value":487},{"type":26,"value":769},{"type":21,"tag":325,"props":1190,"children":1194},{"className":1191,"code":1192,"language":1193,"meta":8,"style":8},"language-sql shiki shiki-themes github-light github-dark","LISTEN broadcast;\n","sql",[1195],{"type":21,"tag":29,"props":1196,"children":1197},{"__ignoreMap":8},[1198],{"type":21,"tag":335,"props":1199,"children":1200},{"class":337,"line":338},[1201],{"type":21,"tag":335,"props":1202,"children":1204},{"style":1203},"--shiki-default:#24292E;--shiki-dark:#E1E4E8",[1205],{"type":26,"value":1192},{"type":21,"tag":22,"props":1207,"children":1208},{},[1209],{"type":26,"value":1210},"User \"Abe\" delivers a message to the group on channel \"broadcast\":",{"type":21,"tag":325,"props":1212,"children":1214},{"className":1191,"code":1213,"language":1193,"meta":8,"style":8},"-- Abe saves a message:\ninsert into message (sender, channel, content)\n     select id, 'broadcast', 'Hello, everone!'\n       from appuser where fullname = 'Abe';\nINSERT 0 1\nAsynchronous notification \"broadcast\" with payload \"{\"id\":2,\"sender\":1,\"channel\":\"broadcast\",\"content\":\"Hello, everone!\"}\" received from server process with PID 61006.\n",[1215],{"type":21,"tag":29,"props":1216,"children":1217},{"__ignoreMap":8},[1218,1227,1246,1275,1313,1331],{"type":21,"tag":335,"props":1219,"children":1220},{"class":337,"line":338},[1221],{"type":21,"tag":335,"props":1222,"children":1224},{"style":1223},"--shiki-default:#6A737D;--shiki-dark:#6A737D",[1225],{"type":26,"value":1226},"-- Abe saves a message:\n",{"type":21,"tag":335,"props":1228,"children":1229},{"class":337,"line":347},[1230,1236,1241],{"type":21,"tag":335,"props":1231,"children":1233},{"style":1232},"--shiki-default:#D73A49;--shiki-dark:#F97583",[1234],{"type":26,"value":1235},"insert into",{"type":21,"tag":335,"props":1237,"children":1238},{"style":1232},[1239],{"type":26,"value":1240}," message",{"type":21,"tag":335,"props":1242,"children":1243},{"style":1203},[1244],{"type":26,"value":1245}," (sender, channel, content)\n",{"type":21,"tag":335,"props":1247,"children":1248},{"class":337,"line":356},[1249,1254,1259,1265,1270],{"type":21,"tag":335,"props":1250,"children":1251},{"style":1232},[1252],{"type":26,"value":1253},"     select",{"type":21,"tag":335,"props":1255,"children":1256},{"style":1203},[1257],{"type":26,"value":1258}," id, ",{"type":21,"tag":335,"props":1260,"children":1262},{"style":1261},"--shiki-default:#032F62;--shiki-dark:#9ECBFF",[1263],{"type":26,"value":1264},"'broadcast'",{"type":21,"tag":335,"props":1266,"children":1267},{"style":1203},[1268],{"type":26,"value":1269},", ",{"type":21,"tag":335,"props":1271,"children":1272},{"style":1261},[1273],{"type":26,"value":1274},"'Hello, everone!'\n",{"type":21,"tag":335,"props":1276,"children":1277},{"class":337,"line":365},[1278,1283,1288,1293,1298,1303,1308],{"type":21,"tag":335,"props":1279,"children":1280},{"style":1232},[1281],{"type":26,"value":1282},"       from",{"type":21,"tag":335,"props":1284,"children":1285},{"style":1203},[1286],{"type":26,"value":1287}," appuser ",{"type":21,"tag":335,"props":1289,"children":1290},{"style":1232},[1291],{"type":26,"value":1292},"where",{"type":21,"tag":335,"props":1294,"children":1295},{"style":1203},[1296],{"type":26,"value":1297}," fullname ",{"type":21,"tag":335,"props":1299,"children":1300},{"style":1232},[1301],{"type":26,"value":1302},"=",{"type":21,"tag":335,"props":1304,"children":1305},{"style":1261},[1306],{"type":26,"value":1307}," 'Abe'",{"type":21,"tag":335,"props":1309,"children":1310},{"style":1203},[1311],{"type":26,"value":1312},";\n",{"type":21,"tag":335,"props":1314,"children":1315},{"class":337,"line":449},[1316,1320,1326],{"type":21,"tag":335,"props":1317,"children":1318},{"style":1232},[1319],{"type":26,"value":1176},{"type":21,"tag":335,"props":1321,"children":1323},{"style":1322},"--shiki-default:#005CC5;--shiki-dark:#79B8FF",[1324],{"type":26,"value":1325}," 0",{"type":21,"tag":335,"props":1327,"children":1328},{"style":1322},[1329],{"type":26,"value":1330}," 1\n",{"type":21,"tag":335,"props":1332,"children":1333},{"class":337,"line":458},[1334,1339,1343,1348,1353,1358,1363,1368,1373,1378,1383,1388,1393,1398,1403,1408,1412,1417,1422,1427,1432,1437,1442,1447,1452,1457],{"type":21,"tag":335,"props":1335,"children":1336},{"style":1203},[1337],{"type":26,"value":1338},"Asynchronous ",{"type":21,"tag":335,"props":1340,"children":1341},{"style":1232},[1342],{"type":26,"value":1140},{"type":21,"tag":335,"props":1344,"children":1345},{"style":1261},[1346],{"type":26,"value":1347}," \"broadcast\"",{"type":21,"tag":335,"props":1349,"children":1350},{"style":1232},[1351],{"type":26,"value":1352}," with",{"type":21,"tag":335,"props":1354,"children":1355},{"style":1203},[1356],{"type":26,"value":1357}," payload ",{"type":21,"tag":335,"props":1359,"children":1360},{"style":1261},[1361],{"type":26,"value":1362},"\"{\"",{"type":21,"tag":335,"props":1364,"children":1365},{"style":1203},[1366],{"type":26,"value":1367},"id",{"type":21,"tag":335,"props":1369,"children":1370},{"style":1261},[1371],{"type":26,"value":1372},"\":2,\"",{"type":21,"tag":335,"props":1374,"children":1375},{"style":1203},[1376],{"type":26,"value":1377},"sender",{"type":21,"tag":335,"props":1379,"children":1380},{"style":1261},[1381],{"type":26,"value":1382},"\":1,\"",{"type":21,"tag":335,"props":1384,"children":1385},{"style":1203},[1386],{"type":26,"value":1387},"channel",{"type":21,"tag":335,"props":1389,"children":1390},{"style":1261},[1391],{"type":26,"value":1392},"\":\"",{"type":21,"tag":335,"props":1394,"children":1395},{"style":1203},[1396],{"type":26,"value":1397},"broadcast",{"type":21,"tag":335,"props":1399,"children":1400},{"style":1261},[1401],{"type":26,"value":1402},"\",\"",{"type":21,"tag":335,"props":1404,"children":1405},{"style":1203},[1406],{"type":26,"value":1407},"content",{"type":21,"tag":335,"props":1409,"children":1410},{"style":1261},[1411],{"type":26,"value":1392},{"type":21,"tag":335,"props":1413,"children":1414},{"style":1203},[1415],{"type":26,"value":1416},"Hello, everone!",{"type":21,"tag":335,"props":1418,"children":1419},{"style":1261},[1420],{"type":26,"value":1421},"\"}\"",{"type":21,"tag":335,"props":1423,"children":1424},{"style":1203},[1425],{"type":26,"value":1426}," received ",{"type":21,"tag":335,"props":1428,"children":1429},{"style":1232},[1430],{"type":26,"value":1431},"from",{"type":21,"tag":335,"props":1433,"children":1434},{"style":1232},[1435],{"type":26,"value":1436}," server",{"type":21,"tag":335,"props":1438,"children":1439},{"style":1203},[1440],{"type":26,"value":1441}," process ",{"type":21,"tag":335,"props":1443,"children":1444},{"style":1232},[1445],{"type":26,"value":1446},"with",{"type":21,"tag":335,"props":1448,"children":1449},{"style":1203},[1450],{"type":26,"value":1451}," PID ",{"type":21,"tag":335,"props":1453,"children":1454},{"style":1322},[1455],{"type":26,"value":1456},"61006",{"type":21,"tag":335,"props":1458,"children":1459},{"style":1203},[1460],{"type":26,"value":1461},".\n",{"type":21,"tag":22,"props":1463,"children":1464},{},[1465],{"type":26,"value":1466},"We're also protected from false-notifications (which we would be susceptible to if we managed notifcations in application logic, i.e., first complete the transaction, followed by manually sending notifications).  Let's say \"Bob\" sends a message as part of a large transaction that fails:",{"type":21,"tag":325,"props":1468,"children":1470},{"className":327,"code":1469,"language":329,"meta":8,"style":8},"demo=# begin;\nBEGIN\ndemo=*# -- insert lots of work in the transaction here\ndemo=*#\ndemo=*# -- Bob saves a message:\ndemo=*# insert into message (sender, channel, content)\ndemo-*>      select id, 'broadcast', 'I just completed the...'\ndemo-*>        from appuser where fullname = 'Bob';\nINSERT 0 1\ndemo=*#\ndemo=*# -- insert lots more work in the transaction here that fails\ndemo=*# rollback;\nROLLBACK\n",[1471],{"type":21,"tag":29,"props":1472,"children":1473},{"__ignoreMap":8},[1474,1481,1488,1496,1504,1512,1520,1528,1536,1544,1551,1559,1566],{"type":21,"tag":335,"props":1475,"children":1476},{"class":337,"line":338},[1477],{"type":21,"tag":335,"props":1478,"children":1479},{},[1480],{"type":26,"value":1023},{"type":21,"tag":335,"props":1482,"children":1483},{"class":337,"line":347},[1484],{"type":21,"tag":335,"props":1485,"children":1486},{},[1487],{"type":26,"value":1031},{"type":21,"tag":335,"props":1489,"children":1490},{"class":337,"line":356},[1491],{"type":21,"tag":335,"props":1492,"children":1493},{},[1494],{"type":26,"value":1495},"demo=*# -- insert lots of work in the transaction here\n",{"type":21,"tag":335,"props":1497,"children":1498},{"class":337,"line":365},[1499],{"type":21,"tag":335,"props":1500,"children":1501},{},[1502],{"type":26,"value":1503},"demo=*#\n",{"type":21,"tag":335,"props":1505,"children":1506},{"class":337,"line":449},[1507],{"type":21,"tag":335,"props":1508,"children":1509},{},[1510],{"type":26,"value":1511},"demo=*# -- Bob saves a message:\n",{"type":21,"tag":335,"props":1513,"children":1514},{"class":337,"line":458},[1515],{"type":21,"tag":335,"props":1516,"children":1517},{},[1518],{"type":26,"value":1519},"demo=*# insert into message (sender, channel, content)\n",{"type":21,"tag":335,"props":1521,"children":1522},{"class":337,"line":467},[1523],{"type":21,"tag":335,"props":1524,"children":1525},{},[1526],{"type":26,"value":1527},"demo-*>      select id, 'broadcast', 'I just completed the...'\n",{"type":21,"tag":335,"props":1529,"children":1530},{"class":337,"line":556},[1531],{"type":21,"tag":335,"props":1532,"children":1533},{},[1534],{"type":26,"value":1535},"demo-*>        from appuser where fullname = 'Bob';\n",{"type":21,"tag":335,"props":1537,"children":1538},{"class":337,"line":565},[1539],{"type":21,"tag":335,"props":1540,"children":1541},{},[1542],{"type":26,"value":1543},"INSERT 0 1\n",{"type":21,"tag":335,"props":1545,"children":1546},{"class":337,"line":574},[1547],{"type":21,"tag":335,"props":1548,"children":1549},{},[1550],{"type":26,"value":1503},{"type":21,"tag":335,"props":1552,"children":1553},{"class":337,"line":583},[1554],{"type":21,"tag":335,"props":1555,"children":1556},{},[1557],{"type":26,"value":1558},"demo=*# -- insert lots more work in the transaction here that fails\n",{"type":21,"tag":335,"props":1560,"children":1561},{"class":337,"line":592},[1562],{"type":21,"tag":335,"props":1563,"children":1564},{},[1565],{"type":26,"value":1054},{"type":21,"tag":335,"props":1567,"children":1568},{"class":337,"line":601},[1569],{"type":21,"tag":335,"props":1570,"children":1571},{},[1572],{"type":26,"value":1062},{"type":21,"tag":22,"props":1574,"children":1575},{},[1576],{"type":26,"value":1577},"No notification delivered!",{"type":21,"tag":168,"props":1579,"children":1581},{"id":1580},"in-python",[1582],{"type":26,"value":1583},"In Python",{"type":21,"tag":22,"props":1585,"children":1586},{},[1587,1589,1594,1596,1603],{"type":26,"value":1588},"As noted above, our architecture uses ",{"type":21,"tag":29,"props":1590,"children":1592},{"className":1591},[],[1593],{"type":26,"value":115},{"type":26,"value":1595}," which has support for ",{"type":21,"tag":78,"props":1597,"children":1600},{"href":1598,"rel":1599},"https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.connection.Connection.add_listener",[82],[1601],{"type":26,"value":1602},"registering callbacks",{"type":26,"value":1604}," on notifications.",{"type":21,"tag":22,"props":1606,"children":1607},{},[1608,1610,1615],{"type":26,"value":1609},"First we must create a virtualenv to install ",{"type":21,"tag":29,"props":1611,"children":1613},{"className":1612},[],[1614],{"type":26,"value":115},{"type":26,"value":695},{"type":21,"tag":325,"props":1617,"children":1619},{"className":327,"code":1618,"language":329,"meta":8,"style":8},"$ python3.11 -m venv venv\n$ source venv/bin/activate\n(venv) $ pip install asyncpg\n",[1620],{"type":21,"tag":29,"props":1621,"children":1622},{"__ignoreMap":8},[1623,1631,1639],{"type":21,"tag":335,"props":1624,"children":1625},{"class":337,"line":338},[1626],{"type":21,"tag":335,"props":1627,"children":1628},{},[1629],{"type":26,"value":1630},"$ python3.11 -m venv venv\n",{"type":21,"tag":335,"props":1632,"children":1633},{"class":337,"line":347},[1634],{"type":21,"tag":335,"props":1635,"children":1636},{},[1637],{"type":26,"value":1638},"$ source venv/bin/activate\n",{"type":21,"tag":335,"props":1640,"children":1641},{"class":337,"line":356},[1642],{"type":21,"tag":335,"props":1643,"children":1644},{},[1645],{"type":26,"value":1646},"(venv) $ pip install asyncpg\n",{"type":21,"tag":959,"props":1648,"children":1650},{"id":1649},"simple-demo",[1651],{"type":26,"value":1652},"Simple Demo",{"type":21,"tag":22,"props":1654,"children":1655},{},[1656,1658,1663,1665,1670,1672,1678],{"type":26,"value":1657},"The python script, ",{"type":21,"tag":29,"props":1659,"children":1661},{"className":1660},[],[1662],{"type":26,"value":393},{"type":26,"value":1664},", establishes a connection to PostgreSQL using the ",{"type":21,"tag":29,"props":1666,"children":1668},{"className":1667},[],[1669],{"type":26,"value":115},{"type":26,"value":1671}," package and adds a listener for notifications on channel ",{"type":21,"tag":29,"props":1673,"children":1675},{"className":1674},[],[1676],{"type":26,"value":1677},"\"broadcast\"",{"type":26,"value":1679},".  It then simulates a server waiting on requests by sleeping.",{"type":21,"tag":325,"props":1681,"children":1683},{"className":327,"code":1682,"language":329,"meta":8,"style":8},"(venv) $ python demo.py\nType Ctrl-c to exit\nMain task Waiting for notifications...\n",[1684],{"type":21,"tag":29,"props":1685,"children":1686},{"__ignoreMap":8},[1687,1695,1703],{"type":21,"tag":335,"props":1688,"children":1689},{"class":337,"line":338},[1690],{"type":21,"tag":335,"props":1691,"children":1692},{},[1693],{"type":26,"value":1694},"(venv) $ python demo.py\n",{"type":21,"tag":335,"props":1696,"children":1697},{"class":337,"line":347},[1698],{"type":21,"tag":335,"props":1699,"children":1700},{},[1701],{"type":26,"value":1702},"Type Ctrl-c to exit\n",{"type":21,"tag":335,"props":1704,"children":1705},{"class":337,"line":356},[1706],{"type":21,"tag":335,"props":1707,"children":1708},{},[1709],{"type":26,"value":1710},"Main task Waiting for notifications...\n",{"type":21,"tag":22,"props":1712,"children":1713},{},[1714,1716,1721,1723,1728,1730,1735],{"type":26,"value":1715},"With the application running in one terminal, run ",{"type":21,"tag":29,"props":1717,"children":1719},{"className":1718},[],[1720],{"type":26,"value":487},{"type":26,"value":1722}," in another.  In that session, when an ",{"type":21,"tag":29,"props":1724,"children":1726},{"className":1725},[],[1727],{"type":26,"value":1176},{"type":26,"value":1729}," on table ",{"type":21,"tag":29,"props":1731,"children":1733},{"className":1732},[],[1734],{"type":26,"value":1160},{"type":26,"value":1736}," is committed, we will see our python application report it:",{"type":21,"tag":325,"props":1738,"children":1740},{"className":1191,"code":1739,"language":1193,"meta":8,"style":8},"insert into message (sender, channel, content)\n     select id, 'broadcast', 'Hello, demo application!'\n       from appuser where fullname = 'Abe';\n",[1741],{"type":21,"tag":29,"props":1742,"children":1743},{"__ignoreMap":8},[1744,1759,1783],{"type":21,"tag":335,"props":1745,"children":1746},{"class":337,"line":338},[1747,1751,1755],{"type":21,"tag":335,"props":1748,"children":1749},{"style":1232},[1750],{"type":26,"value":1235},{"type":21,"tag":335,"props":1752,"children":1753},{"style":1232},[1754],{"type":26,"value":1240},{"type":21,"tag":335,"props":1756,"children":1757},{"style":1203},[1758],{"type":26,"value":1245},{"type":21,"tag":335,"props":1760,"children":1761},{"class":337,"line":347},[1762,1766,1770,1774,1778],{"type":21,"tag":335,"props":1763,"children":1764},{"style":1232},[1765],{"type":26,"value":1253},{"type":21,"tag":335,"props":1767,"children":1768},{"style":1203},[1769],{"type":26,"value":1258},{"type":21,"tag":335,"props":1771,"children":1772},{"style":1261},[1773],{"type":26,"value":1264},{"type":21,"tag":335,"props":1775,"children":1776},{"style":1203},[1777],{"type":26,"value":1269},{"type":21,"tag":335,"props":1779,"children":1780},{"style":1261},[1781],{"type":26,"value":1782},"'Hello, demo application!'\n",{"type":21,"tag":335,"props":1784,"children":1785},{"class":337,"line":356},[1786,1790,1794,1798,1802,1806,1810],{"type":21,"tag":335,"props":1787,"children":1788},{"style":1232},[1789],{"type":26,"value":1282},{"type":21,"tag":335,"props":1791,"children":1792},{"style":1203},[1793],{"type":26,"value":1287},{"type":21,"tag":335,"props":1795,"children":1796},{"style":1232},[1797],{"type":26,"value":1292},{"type":21,"tag":335,"props":1799,"children":1800},{"style":1203},[1801],{"type":26,"value":1297},{"type":21,"tag":335,"props":1803,"children":1804},{"style":1232},[1805],{"type":26,"value":1302},{"type":21,"tag":335,"props":1807,"children":1808},{"style":1261},[1809],{"type":26,"value":1307},{"type":21,"tag":335,"props":1811,"children":1812},{"style":1203},[1813],{"type":26,"value":1312},{"type":21,"tag":22,"props":1815,"children":1816},{},[1817],{"type":26,"value":1818},"In the other terminal we will see:",{"type":21,"tag":325,"props":1820,"children":1822},{"className":327,"code":1821,"language":329,"meta":8,"style":8},"RECEIVED notification from \u003Casyncpg.connection.Connection object at 0x7f23f93b7920>[pid: 202] on channel broadcast:\n{\n    \"id\": 9,\n    \"sender\": 1,\n    \"channel\": \"broadcast\",\n    \"content\": \"Hello, demo application!\"\n}\n------------------------------------------------------------------------------\n",[1823],{"type":21,"tag":29,"props":1824,"children":1825},{"__ignoreMap":8},[1826,1834,1842,1850,1858,1866,1874,1882],{"type":21,"tag":335,"props":1827,"children":1828},{"class":337,"line":338},[1829],{"type":21,"tag":335,"props":1830,"children":1831},{},[1832],{"type":26,"value":1833},"RECEIVED notification from \u003Casyncpg.connection.Connection object at 0x7f23f93b7920>[pid: 202] on channel broadcast:\n",{"type":21,"tag":335,"props":1835,"children":1836},{"class":337,"line":347},[1837],{"type":21,"tag":335,"props":1838,"children":1839},{},[1840],{"type":26,"value":1841},"{\n",{"type":21,"tag":335,"props":1843,"children":1844},{"class":337,"line":356},[1845],{"type":21,"tag":335,"props":1846,"children":1847},{},[1848],{"type":26,"value":1849},"    \"id\": 9,\n",{"type":21,"tag":335,"props":1851,"children":1852},{"class":337,"line":365},[1853],{"type":21,"tag":335,"props":1854,"children":1855},{},[1856],{"type":26,"value":1857},"    \"sender\": 1,\n",{"type":21,"tag":335,"props":1859,"children":1860},{"class":337,"line":449},[1861],{"type":21,"tag":335,"props":1862,"children":1863},{},[1864],{"type":26,"value":1865},"    \"channel\": \"broadcast\",\n",{"type":21,"tag":335,"props":1867,"children":1868},{"class":337,"line":458},[1869],{"type":21,"tag":335,"props":1870,"children":1871},{},[1872],{"type":26,"value":1873},"    \"content\": \"Hello, demo application!\"\n",{"type":21,"tag":335,"props":1875,"children":1876},{"class":337,"line":467},[1877],{"type":21,"tag":335,"props":1878,"children":1879},{},[1880],{"type":26,"value":1881},"}\n",{"type":21,"tag":335,"props":1883,"children":1884},{"class":337,"line":556},[1885],{"type":21,"tag":335,"props":1886,"children":1887},{},[1888],{"type":26,"value":1889},"------------------------------------------------------------------------------\n",{"type":21,"tag":168,"props":1891,"children":1893},{"id":1892},"summary",[1894],{"type":26,"value":1895},"Summary",{"type":21,"tag":22,"props":1897,"children":1898},{},[1899,1901,1906,1907,1912,1914,1919],{"type":26,"value":1900},"For applications already using PostgreSQL you can use ",{"type":21,"tag":29,"props":1902,"children":1904},{"className":1903},[],[1905],{"type":26,"value":723},{"type":26,"value":1269},{"type":21,"tag":29,"props":1908,"children":1910},{"className":1909},[],[1911],{"type":26,"value":739},{"type":26,"value":1913},", and ",{"type":21,"tag":29,"props":1915,"children":1917},{"className":1916},[],[1918],{"type":26,"value":755},{"type":26,"value":1920}," to manage pub/sub for your application without having to install redis solely to add pub/sub infrastructure, allowing you to reduce the footprint of your application suite.",{"type":21,"tag":22,"props":1922,"children":1923},{},[1924,1926,1931],{"type":26,"value":1925},"By placing ",{"type":21,"tag":29,"props":1927,"children":1929},{"className":1928},[],[1930],{"type":26,"value":723},{"type":26,"value":1932}," in transactions, and more specifically, triggers, you can make your notifications data-driven.",{"type":21,"tag":22,"props":1934,"children":1935},{},[1936,1938,1943],{"type":26,"value":1937},"For asynchronous applications using ",{"type":21,"tag":29,"props":1939,"children":1941},{"className":1940},[],[1942],{"type":26,"value":115},{"type":26,"value":1944},", an API exists for registering callbacks which will be run asynchronously as notifications are delivered.",{"type":21,"tag":680,"props":1946,"children":1947},{},[1948],{"type":21,"tag":22,"props":1949,"children":1950},{},[1951,1953,1964,1966,1973],{"type":26,"value":1952},"NOTE, not shown here: if you're using synchronous python with ",{"type":21,"tag":78,"props":1954,"children":1957},{"href":1955,"rel":1956},"https://www.psycopg.org/docs/",[82],[1958],{"type":21,"tag":29,"props":1959,"children":1961},{"className":1960},[],[1962],{"type":26,"value":1963},"psycopg",{"type":26,"value":1965},", the library has support for ",{"type":21,"tag":78,"props":1967,"children":1970},{"href":1968,"rel":1969},"https://www.psycopg.org/docs/advanced.html#asynchronous-notifications",[82],[1971],{"type":26,"value":1972},"receiving notifications",{"type":26,"value":695},{"type":21,"tag":1975,"props":1976,"children":1977},"style",{},[1978],{"type":26,"value":1979},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":8,"searchDepth":356,"depth":356,"links":1981},[1982,1984,1985,1986,1990,1993],{"id":170,"depth":347,"text":1983},"Live Updates",{"id":268,"depth":347,"text":271},{"id":306,"depth":347,"text":309},{"id":698,"depth":347,"text":701,"children":1987},[1988],{"id":961,"depth":356,"text":1989},"NOTIFY in Transactions",{"id":1580,"depth":347,"text":1583,"children":1991},[1992],{"id":1649,"depth":356,"text":1652},{"id":1892,"depth":347,"text":1895},"markdown","content:dpopowich:2023-8:postgres-pubsub.md","dpopowich/2023-8/postgres-pubsub.md","dpopowich/2023-8/postgres-pubsub","md",{"user":2000,"name":2001},"dpopowich","Daniel Popowich",1780330265500]