data inserted into db after spring transaction rollback -


i'm testing declartive transaction configuration following testcase:

i try insert 2 record table 4 columns(id, content, position, time) while position unique index. use mysql 5.5 innodb engine, , develop test spring 3.2.2, mybatis 3.2.2, mybatis-spring 1.2.0. below sql create database , table experimental data inserted.

     create database `development` default character set utf8 collate utf8_general_ci;     use `development`;      create table if not exists `test` (       `id` int(10) unsigned not null auto_increment,       `content` varchar(256) default null,       `position` int(10) unsigned not null,       `time` int(10) unsigned not null default '0',       primary key (`id`),       unique key `position` (`position`)     ) engine=innodb  default charset=utf8 auto_increment=97 ;      insert `test` (`id`, `content`, `position`, `time`) values (1, 'test', 0, 1368164281),(2, '测试内容', 1, 1368164364),(44, 'bbb', 2, 1368431459),(45, 'ccc', 3, 1368431459), 

here configuration xml service.xml:

<beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"     xmlns:aop="http://www.springframework.org/schema/aop"     xmlns:tx="http://www.springframework.org/schema/tx"     xsi:schemalocation="     http://www.springframework.org/schema/beans     http://www.springframework.org/schema/beans/spring-beans.xsd     http://www.springframework.org/schema/tx     http://www.springframework.org/schema/tx/spring-tx.xsd     http://www.springframework.org/schema/aop     http://www.springframework.org/schema/aop/spring-aop.xsd">     <bean id="testdatasource" class="com.mchange.v2.c3p0.combopooleddatasource" destroy-method="close">         <property name="driverclassname" value="${test.driverclass}" />         <property name="url" value="${test.jdbcurl}" />         <property name="username" value="${test.user}" />         <property name="password" value="${test.password}" />         <property name="minpoolsize" value="${test.minipoolsize}" />         <property name="maxpoolsize" value="${test.maxpoolsize}" />         <property name="initialpoolsize" value="${test.initialpoolsize}" />         <property name="maxidletime" value="${test.maxidletime}" />         <property name="acquireincrement" value="${test.acquireincrement}" />         <property name="acquireretryattempts" value="${test.acquireretryattempts}" />         <property name="acquireretrydelay" value="${test.acquireretrydelay}" />         <property name="testconnectiononcheckin" value="${test.testconnectiononcheckin}" />         <property name="automatictesttable" value="${test.automatictesttable}" />         <property name="idleconnectiontestperiod" value="${test.idleconnectiontestperiod}" />         <property name="checkouttimeout" value="${test.checkouttimeout}" />     </bean>      <bean id="txmanager" class="org.springframework.jdbc.datasource.datasourcetransactionmanager">           <property name="datasource" ref="testdatasource" />     </bean>      <tx:advice id="txadvice" transaction-manager="txmanager">         <tx:attributes>             <tx:method name="get*" read-only="true" />             <tx:method name="add*" />         </tx:attributes>     </tx:advice>     <aop:config>         <aop:pointcut id="testserviceoperation" expression="execution(* com.ssports.test.service.testserviceimpl.addrecords(..))" />         <aop:advisor advice-ref="txadvice" pointcut-ref="testserviceoperation" />     </aop:config>      <bean id="testservice" class="com.ssports.test.service.testserviceimpl" /> </beans> 

below test-mapper-config.xml:

<?xml version="1.0" encoding="utf-8"?> <beans xmlns="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"     xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"     xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd">      <bean id="recordmapper" class="org.mybatis.spring.mapper.mapperfactorybean">     <property name="sqlsessionfactory" ref="testsqlsessionfactory" />     <property name="mapperinterface" value="com.ssports.test.mapper.recordmapper" />     </bean> </beans> 

here serviceimpl class:

package com.ssports.test.service;  import java.util.list;  import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.jdbc.datasource.datasourcetransactionmanager;  import com.ssports.test.mapper.recordmapper; import com.ssports.test.model.record; import com.ssports.test.model.recordexample; import com.ssports.util.springhelper;  public class testserviceimpl implements testservice {     logger logger = loggerfactory.getlogger(testserviceimpl.class);     private static recordmapper mapper = springhelper.getbean("recordmapper");      public list<record> getall() {         return mapper.selectbyexample(new recordexample());     }      public record getrecordbyid(int id) {         return mapper.selectbyprimarykey(id);     }      public void addrecords(list<record> recordlist) throws exception {            (record record : recordlist) {                 logger.info(record.getposition() + ":" + record.gettime());                     mapper.insert(record);         }     }  } 

the test code is: package com.ssports.db;

import java.util.arraylist; import java.util.date; import java.util.list;   import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.jdbc.datasource.datasourcetransactionmanager;  import com.ssports.test.model.record; import com.ssports.test.service.testservice; import com.ssports.util.springhelper;  public class transtest {     /**      * @param args      * @throws exception      */     public static void main(string[] args) throws exception {         logger logger = loggerfactory.getlogger(transtest.class);          testservice service = springhelper.getbean("testservice");         list<record> records = new arraylist<record>();          record record1 = service.getrecordbyid(1);         record record4 = new record();         record4.setcontent("ddd");         record4.setposition(4);         record4.settime((int) (new long(system.currenttimemillis()) / 1000));          records.add(record4);         records.add(record1);          datasourcetransactionmanager txmanager = springhelper.getbean("txmanager");         try {             service.addrecords(records);         } catch (unsupportedoperationexception ex) {                         logger.info(ex.getmessage());         }          list<record> recordlist = service.getall();         (record item : recordlist) {             logger.info(item.getid() + ":" + item.getcontent() + ":"                     + new date((long) item.gettime() * 1000) + ":"                     + item.getposition());         }      }  } 

i run application, , here log:

debug: org.springframework.transaction.interceptor.namematchtransactionattributesource - adding transactional method [get*] attribute [propagation_required,isolation_default,readonly] debug: org.springframework.transaction.interceptor.namematchtransactionattributesource - adding transactional method [add*] attribute [propagation_required,isolation_default] debug: org.springframework.transaction.interceptor.namematchtransactionattributesource - adding transactional method [get*] attribute [propagation_required,isolation_default,readonly] debug: org.springframework.transaction.interceptor.namematchtransactionattributesource - adding transactional method [add*] attribute [propagation_required,isolation_default] debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - creating new transaction name [com.ssports.test.service.testserviceimpl.addrecords]: propagation_required,isolation_default debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - acquired connection [org.apache.commons.dbcp.poolableconnection@6650af3b] jdbc transaction debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - switching jdbc connection [org.apache.commons.dbcp.poolableconnection@6650af3b] manual commit info : com.ssports.test.service.testserviceimpl - 4:1368602606 info : com.ssports.test.service.testserviceimpl - 0:1368164281 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - initiating transaction rollback debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - rolling jdbc transaction on connection [org.apache.commons.dbcp.poolableconnection@6650af3b] debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - releasing jdbc connection [org.apache.commons.dbcp.poolableconnection@6650af3b] after transaction info : com.ssports.db.transtest -  ### error updating database.  cause: com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '1' key 'primary' ### error may involve com.ssports.test.mapper.recordmapper.insert-inline ### error occurred while setting parameters ### sql: insert test (id, content, position,        time)     values (?, ?, ?,        ?) ### cause: com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '1' key 'primary' ; sql []; duplicate entry '1' key 'primary'; nested exception com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '1' key 'primary' info : com.ssports.db.transtest - 1:test:fri may 10 13:38:01 cst 2013:0 info : com.ssports.db.transtest - 2:测试内容:fri may 10 13:39:24 cst 2013:1 info : com.ssports.db.transtest - 44:bbb:mon may 13 15:50:59 cst 2013:2 info : com.ssports.db.transtest - 45:ccc:mon may 13 15:50:59 cst 2013:3 info : com.ssports.db.transtest - 98:ddd:wed may 15 15:23:26 cst 2013:4 

you can see while exception throw, transaction manager roll database back, record still insert it.

is there can tell wrong here, or bug?

updated

i tried programatic transaction code below, of course comment lines in service.xml disable aop , txadvice. after that,it works:

package com.ssports.test.service;  import java.util.list;  import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.jdbc.datasource.datasourcetransactionmanager; import org.springframework.transaction.transactiondefinition; import org.springframework.transaction.transactionstatus; import org.springframework.transaction.support.defaulttransactiondefinition;  import com.ssports.test.mapper.recordmapper; import com.ssports.test.model.record; import com.ssports.test.model.recordexample; import com.ssports.util.springhelper;  public class testserviceimpl implements testservice {     logger logger = loggerfactory.getlogger(testserviceimpl.class);     private static recordmapper mapper = springhelper.getbean("recordmapper");     private static datasourcetransactionmanager txmanager = springhelper.getbean("txmanager");     private static transactiondefinition def = new defaulttransactiondefinition();     private static transactionstatus status = txmanager.gettransaction(def);      public list<record> getall() {         return mapper.selectbyexample(new recordexample());     }      public record getrecordbyid(int id) {         thread t = thread.currentthread();         logger.debug("thread name: "+t.getname());         logger.debug("thread id: "+t.getid());         return mapper.selectbyprimarykey(id);     }      public void addrecords(list<record> recordlist) throws exception {           try {         (record record : recordlist) {             thread t = thread.currentthread();             logger.debug("thread name: "+t.getname());             logger.debug("thread id: "+t.getid());             logger.info(record.getposition() + ":" + record.gettime());             mapper.insert(record);         }         } catch (exception ex) {             logger.debug("exception throw");             logger.debug(ex.getmessage());             txmanager.rollback(status);             throw ex;         }          txmanager.commit(status);     }  } 

here log:

2013-05-16 11:23:17 625 debug: org.mybatis.spring.sqlsessionfactorybean - parsed configuration file: 'class path resource [config/mybatis-config.xml]' 2013-05-16 11:23:17 929 debug: org.mybatis.spring.sqlsessionfactorybean - parsed mapper file: 'file [/home/zhangzhi/workspace-sts/multidb/target/classes/com/ssports/uc/mybatis/mappers/ucmembermapper.xml]' 2013-05-16 11:23:17 948 debug: org.mybatis.spring.sqlsessionfactorybean - parsed configuration file: 'class path resource [config/mybatis-config.xml]' 2013-05-16 11:23:18 247 debug: org.mybatis.spring.sqlsessionfactorybean - parsed mapper file: 'file [/home/zhangzhi/workspace-sts/multidb/target/classes/com/ssports/site/mybatis/mappers/admapper.xml]' 2013-05-16 11:23:18 264 debug: org.mybatis.spring.sqlsessionfactorybean - parsed configuration file: 'class path resource [config/mybatis-config.xml]' 2013-05-16 11:23:18 316 debug: org.mybatis.spring.sqlsessionfactorybean - parsed mapper file: 'file [/home/zhangzhi/workspace-sts/multidb/target/classes/com/ssports/test/mybatis/mappers/recordmapper.xml]' 2013-05-16 11:23:18 476 debug: org.mybatis.spring.sqlsessionfactorybean - parsed configuration file: 'class path resource [config/mybatis-config.xml]' 2013-05-16 11:23:18 546 debug: org.mybatis.spring.sqlsessionfactorybean - parsed mapper file: 'file [/home/zhangzhi/workspace-sts/multidb/target/classes/com/ssports/uc/mybatis/mappers/ucmembermapper.xml]' 2013-05-16 11:23:18 556 debug: org.mybatis.spring.sqlsessionfactorybean - parsed configuration file: 'class path resource [config/mybatis-config.xml]' 2013-05-16 11:23:18 680 debug: org.mybatis.spring.sqlsessionfactorybean - parsed mapper file: 'file [/home/zhangzhi/workspace-sts/multidb/target/classes/com/ssports/site/mybatis/mappers/admapper.xml]' 2013-05-16 11:23:18 690 debug: org.mybatis.spring.sqlsessionfactorybean - parsed configuration file: 'class path resource [config/mybatis-config.xml]' 2013-05-16 11:23:18 728 debug: org.mybatis.spring.sqlsessionfactorybean - parsed mapper file: 'file [/home/zhangzhi/workspace-sts/multidb/target/classes/com/ssports/test/mybatis/mappers/recordmapper.xml]' 2013-05-16 11:23:18 739 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - creating new transaction name [null]: propagation_required,isolation_default 2013-05-16 11:23:18 740 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - acquired connection [org.apache.commons.dbcp.poolableconnection@6188024d] jdbc transaction 2013-05-16 11:23:18 747 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - switching jdbc connection [org.apache.commons.dbcp.poolableconnection@6188024d] manual commit 2013-05-16 11:23:18 748 debug: com.ssports.db.transtest - record1. 2013-05-16 11:23:18 749 debug: com.ssports.db.transtest - thread name: main 2013-05-16 11:23:18 749 debug: com.ssports.db.transtest - thread id: 1 2013-05-16 11:23:18 749 debug: com.ssports.test.service.testserviceimpl - thread name: main 2013-05-16 11:23:18 749 debug: com.ssports.test.service.testserviceimpl - thread id: 1 2013-05-16 11:23:18 754 debug: org.mybatis.spring.sqlsessionutils - creating new sqlsession 2013-05-16 11:23:18 757 debug: org.mybatis.spring.sqlsessionutils - registering transaction synchronization sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] 2013-05-16 11:23:18 776 debug: org.mybatis.spring.transaction.springmanagedtransaction - jdbc connection [org.apache.commons.dbcp.poolableconnection@6188024d] managed spring 2013-05-16 11:23:18 777 debug: com.ssports.test.mapper.recordmapper.selectbyprimarykey - ooo using connection [org.apache.commons.dbcp.poolableconnection@6188024d] 2013-05-16 11:23:18 783 debug: com.ssports.test.mapper.recordmapper.selectbyprimarykey - ==>  preparing: select id, content, position, time test id = ?  2013-05-16 11:23:18 807 debug: com.ssports.test.mapper.recordmapper.selectbyprimarykey - ==> parameters: 1(integer) 2013-05-16 11:23:18 824 debug: org.mybatis.spring.sqlsessionutils - releasing transactional sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] 2013-05-16 11:23:18 824 debug: com.ssports.db.transtest - invoking service method[addrecords] 2013-05-16 11:23:18 824 debug: com.ssports.db.transtest - thread name: main 2013-05-16 11:23:18 824 debug: com.ssports.db.transtest - thread id: 1 2013-05-16 11:23:18 824 debug: com.ssports.test.service.testserviceimpl - thread name: main 2013-05-16 11:23:18 824 debug: com.ssports.test.service.testserviceimpl - thread id: 1 2013-05-16 11:23:18 824 info : com.ssports.test.service.testserviceimpl - 4:1368674598 2013-05-16 11:23:18 824 debug: org.mybatis.spring.sqlsessionutils - fetched sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] current transaction 2013-05-16 11:23:18 825 debug: com.ssports.test.mapper.recordmapper.insert - ooo using connection [org.apache.commons.dbcp.poolableconnection@6188024d] 2013-05-16 11:23:18 825 debug: com.ssports.test.mapper.recordmapper.insert - ==>  preparing: insert test (id, content, position, time) values (?, ?, ?, ?)  2013-05-16 11:23:18 825 debug: com.ssports.test.mapper.recordmapper.insert - ==> parameters: null, ddd(string), 4(integer), 1368674598(integer) 2013-05-16 11:23:18 826 debug: org.mybatis.spring.sqlsessionutils - releasing transactional sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] 2013-05-16 11:23:18 826 debug: com.ssports.test.service.testserviceimpl - thread name: main 2013-05-16 11:23:18 826 debug: com.ssports.test.service.testserviceimpl - thread id: 1 2013-05-16 11:23:18 826 info : com.ssports.test.service.testserviceimpl - 0:1368164281 2013-05-16 11:23:18 826 debug: org.mybatis.spring.sqlsessionutils - fetched sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] current transaction 2013-05-16 11:23:18 826 debug: com.ssports.test.mapper.recordmapper.insert - ooo using connection [org.apache.commons.dbcp.poolableconnection@6188024d] 2013-05-16 11:23:18 826 debug: com.ssports.test.mapper.recordmapper.insert - ==>  preparing: insert test (id, content, position, time) values (?, ?, ?, ?)  2013-05-16 11:23:18 826 debug: com.ssports.test.mapper.recordmapper.insert - ==> parameters: null, test(string), 0(integer), 1368164281(integer) 2013-05-16 11:23:18 871 debug: org.mybatis.spring.sqlsessionutils - releasing transactional sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] 2013-05-16 11:23:18 871 debug: com.ssports.test.service.testserviceimpl - exception throw 2013-05-16 11:23:18 871 debug: com.ssports.test.service.testserviceimpl -  ### error updating database.  cause: com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '0' key 'position' ### error may involve com.ssports.test.mapper.recordmapper.insert-inline ### error occurred while setting parameters ### sql: insert test (id, content, position,        time)     values (?, ?, ?,        ?) ### cause: com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '0' key 'position' ; sql []; duplicate entry '0' key 'position'; nested exception com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '0' key 'position' 2013-05-16 11:23:18 871 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - initiating transaction rollback 2013-05-16 11:23:18 871 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - rolling jdbc transaction on connection [org.apache.commons.dbcp.poolableconnection@6188024d] 2013-05-16 11:23:18 907 debug: org.mybatis.spring.sqlsessionutils - transaction synchronization rolling sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] 2013-05-16 11:23:18 907 debug: org.mybatis.spring.sqlsessionutils - transaction synchronization closing sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@158b6810] 2013-05-16 11:23:18 908 debug: org.springframework.jdbc.datasource.datasourcetransactionmanager - releasing jdbc connection [org.apache.commons.dbcp.poolableconnection@6188024d] after transaction 2013-05-16 11:23:18 909 debug: com.ssports.db.transtest - exception throw 2013-05-16 11:23:18 909 debug: com.ssports.db.transtest -  ### error updating database.  cause: com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '0' key 'position' ### error may involve com.ssports.test.mapper.recordmapper.insert-inline ### error occurred while setting parameters ### sql: insert test (id, content, position,        time)     values (?, ?, ?,        ?) ### cause: com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '0' key 'position' ; sql []; duplicate entry '0' key 'position'; nested exception com.mysql.jdbc.exceptions.jdbc4.mysqlintegrityconstraintviolationexception: duplicate entry '0' key 'position' 2013-05-16 11:23:18 909 debug: com.ssports.db.transtest - checking result 2013-05-16 11:23:18 909 debug: org.mybatis.spring.sqlsessionutils - creating new sqlsession 2013-05-16 11:23:18 909 debug: org.mybatis.spring.sqlsessionutils - sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@2c8446f7] not registered synchronization because synchronization not active 2013-05-16 11:23:18 929 debug: org.mybatis.spring.transaction.springmanagedtransaction - jdbc connection [org.apache.commons.dbcp.poolableconnection@6188024d] not managed spring 2013-05-16 11:23:18 929 debug: com.ssports.test.mapper.recordmapper.selectbyexample - ooo using connection [org.apache.commons.dbcp.poolableconnection@6188024d] 2013-05-16 11:23:18 929 debug: com.ssports.test.mapper.recordmapper.selectbyexample - ==>  preparing: select id, content, position, time test  2013-05-16 11:23:18 930 debug: com.ssports.test.mapper.recordmapper.selectbyexample - ==> parameters:  2013-05-16 11:23:18 931 debug: org.mybatis.spring.sqlsessionutils - closing non transactional sqlsession [org.apache.ibatis.session.defaults.defaultsqlsession@2c8446f7] 2013-05-16 11:23:18 932 info : com.ssports.db.transtest - 1:test:fri may 10 13:38:01 cst 2013:0 2013-05-16 11:23:18 932 info : com.ssports.db.transtest - 2:测试内容:fri may 10 13:39:24 cst 2013:1 2013-05-16 11:23:18 932 info : com.ssports.db.transtest - 44:bbb:mon may 13 15:50:59 cst 2013:2 2013-05-16 11:23:18 932 info : com.ssports.db.transtest - 45:ccc:mon may 13 15:50:59 cst 2013:3 

and here springhelper class:

package com.ssports.util;  import org.apache.commons.logging.log; import org.apache.commons.logging.logfactory; import org.springframework.context.applicationcontext; import org.springframework.context.support.classpathxmlapplicationcontext;  public class springhelper {      private static log logger = logfactory.getlog(springhelper.class);      private static applicationcontext cx = null;      @suppresswarnings("unchecked")     public static <t> t getbean(string beanid){         if(cx == null){           cx = new classpathxmlapplicationcontext("classpath:spring/application-context.xml");         }         return (t)cx.getbean(beanid);     }      public synchronized static void init(){         if(cx == null){           cx = new classpathxmlapplicationcontext("classpath:spring/application-context.xml");           logger.info("spring config success!,applicationcontext set object");         }     }      public synchronized static void init(string[] paths){          if(cx == null){           cx = new classpathxmlapplicationcontext(paths);           logger.info("spring config success!,applicationcontext set object");         }      }      public synchronized static void init(string path){          init(new string[]{path});     }  } 

updated i‘ve made little change on testserviceimpl, , it's solved, below:

private static recordmapper mapper;  public void init() {     if (null == mapper) {         mapper = (recordmapper) springhelper.getbean("recordmapper");     } } 

the problem:

i problem in springhelper class implementation , how using (namely static reference in testserviceimpl). you might have 2 spring contexts initialized (symptoms contained within first 4 lines of log file).

why not work:

transaction+rollback in 1 context not have effect on db connection made other context (both have own testdatasource , txmanager beans).

how solve it:

spring dependency injection framework, don't see point implementing own dependency lookup strategy. might want check autowired annotation getting depdendencies (such testserviceimpl.mapper) in place.

check spring documentation , demo projects (e.g. https://github.com/springsource/greenhouse) see how best out of spring. try use spring in standard way.

additional aop issue:

btw. pointcut definition not correct read-only advice not covered that.


Comments

Popular posts from this blog

jquery - How can I dynamically add a browser tab? -

node.js - Getting the socket id,user id pair of a logged in user(s) -

keyboard - C++ GetAsyncKeyState alternative -