diff --git a/Makefile b/Makefile index 3c0d748..c8e3120 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ clean: compile: @echo "Running rebar3 compile..." - @$(REBAR3) as compile compile + @$(REBAR3) compile eunit: @echo "Running rebar3 eunit..." diff --git a/README.md b/README.md index f6372d0..4cc9d3a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,184 @@ aspike_protocol ===== -An OTP library +Implementation of Aerospike binary protocol in Erlang + +Binary protocol +--------------- + +Aerospike Binary protocol is described in 'doc/Aerospike_packet_structure.txt'. + +Aerospike Cluster Discovery is described in 'doc/Aerospike_cluster_discovery.txt'. Build ----- - $ rebar3 compile + $ make compile + +Test +----- + + $ make eunit + +Usage +----- +Examples can be run in the following environments: +- Aerospike Cluster Standard/Enterprise/Cloud (https://aerospike.com/products/features-and-editions/); +- Aerospike Cluster Community Edition (CE) (https://hub.docker.com/r/aerospike/aerospike-server); +- Aerospike Server Emulator (https://github.com/vsavkov/aspike-server). + +To start Aerospike Cluster Community edition follow the instructions on https://hub.docker.com/r/aerospike/aerospike-server). + +To start Aerospike Server Emulator: +1. Clone https://github.com/vsavkov/aspike-server; +2. Run command 'iex -S mix' from 'aspike-server' director; +3. Aerospike Server Emulator listens for the Aerospike protocol on port 4041; +4. Aerospike Server Emulator listens for text protocol on port 4040; +5. Use text protocol to create namespace 'test' for examples that involve Key-Value operations: + + Start telnet or nc (aka netcat) in a separate terminal: + + $ nc -vv 127.0.0.1 4040 + + type in: + + CREATE test + + OK + + to check that namespace 'test' exists type in: + + NAMESPACES + + [test] + +Examples +-------- + + $ rebar3 shell + Erlang/OTP ... + +Password encryption +------------------- +Aerospike uses Blowfish cipher to encrypt password that is sent from client to cluster. + +Aerospike has some specifics in Blowfish cipher implementation. + +'aspike_blowfish:crypt/1' implements the Aerospike-specific Blowfish cipher. + +WARNING: The encryption rate of the implementation is slow. + +To encrypt password + + > Encrypted = aspike_blowfish:crypt("password"). + <<"$2a$10$7EqJtq98hPqEX7fNZaFWoOqUH6KN8IKy3Yk5Kz..RHGKNAUCoP/LG">> + +NOTE: Aerospike CE does not require LOGIN, but accept LOGIN as no op. + +Cluster Login +------------- + + > c("examples/login_example.erl"). + % Login to Aerospike CE + > login_example:login("127.0.0.1", 3000, "User", "pwd"). + ok + % Login to Aerospike Emulator + > login_example:login("127.0.0.1", 4041, "User1", "pass1"). + ok + +Cluster Information +------------------- +NOTE: Aerospike Emulator does not support cluster information retrieval at the present time. + + > c("examples/info.erl"). + > info_example:info("127.0.0.1", 3000, "", "", ["build"], 1000). + [{<<"build">>,<<"6.3.0.5">>},{<<>>}] + > info_example:info("127.0.0.1", 3000, "", "", ["namespaces"], 1000). + [{<<"namespaces">>,<<"test">>},{<<>>}] + > info_example:info("127.0.0.1", 3000, "", "", ["partitions", "replicas"], 1000). + [{<<"partitions">>,<<"4096">>}, + {<<"replicas">>, + <<"test:0,1,///////////////////////////////////////////////////////////////////////////////////////////"...>>}, + {<<>>}] + +PUT Key-Value +------------- + > c("examples/put_example.erl"). + % PUT, Aerospike CE + > put_example:put("127.0.0.1", 3000, "", "", "test", "set1", "key1", [{"bin1", "value1"}]). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % PUT, Aerospike Emulator + % Encrypt password for Aerospike Emulator + > Encrypted_password = aspike_blowfish:crypt("pass1"). + > put_example:put("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1", [{"bin1", "value1"}]). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + +GET Key-Value +------------- + > c("examples/get_example.erl"). + % GET, Aerospike CE + > get_example:get("127.0.0.1", 3000, "", "", "test", "set1", "key1", []). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + [], [{<<"bin1">>,"value1"}] + % GET, Aerospike Emulator + > get_example:get("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1", ["bin1"]). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + [], [{<<"bin1">>,"value1"}] + +REMOVE Key +---------- + > c("examples/get_example.erl"). + % REMOVE, Aerospike CE + > remove_example:remove("127.0.0.1", 3000, "", "", "test", "set1", "key1"). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % REMOVE again + > remove_example:remove("127.0.0.1", 3000, "", "", "test", "set1", "key1"). + {2,<<"AEROSPIKE_ERR_RECORD_NOT_FOUND">>, <<"Record does not exist in database.">>} + % REMOVE, Aerospike Emulator + > remove_example:remove("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1"). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % REMOVE again + > remove_example:remove("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1"). + {2,<<"AEROSPIKE_ERR_RECORD_NOT_FOUND">>, <<"Record does not exist in database.">>} + +EXISTS Key +---------- + > c("examples/exists_example.erl"). + + %% Aerospike CE section + % First, PUT Key-Value + > put_example:put("127.0.0.1", 3000, "", "", "test", "set1", "key1", [{"bin1", "value1"}]). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % Next, check that Key EXISTS + > exists_example:exists("127.0.0.1", 3000, "", "", "test", "set1", "key1"). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % GET Key + > get_example:get("127.0.0.1", 3000, "", "", "test", "set1", "key1", []). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + [], [{<<"bin1">>,"value1"}] + % Now, REMOVE Key + > remove_example:remove("127.0.0.1", 3000, "", "", "test", "set1", "key1"). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % Check again, whether the Key exists + > exists_example:exists("127.0.0.1", 3000, "", "", "test", "set1", "key1"). + {2,<<"AEROSPIKE_ERR_RECORD_NOT_FOUND">>, <<"Record does not exist in database.">>} + %% Aerospike CE section. End + + %% Aerospike Emulator section + % First, PUT Key-Value + > put_example:put("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1", [{"bin1", "value1"}]). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % Next, check that Key EXISTS + > exists_example:exists("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1"). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % GET Key + > get_example:get("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1", ["bin1"]). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + [], [{<<"bin1">>,"value1"}] + % Now, REMOVE Key + > remove_example:remove("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1"). + {0,<<"AEROSPIKE_OK">>,<<"Generic success.">>} + % Check again, whether the Key exists + > exists_example:exists("127.0.0.1", 4041, "User1", Encrypted_password, "test", "set1", "key1"). + {2,<<"AEROSPIKE_ERR_RECORD_NOT_FOUND">>, <<"Record does not exist in database.">>} + %% Aerospike Emulator section. End diff --git a/doc/Aerospike_cluster_discovery.txt b/doc/Aerospike_cluster_discovery.txt new file mode 100644 index 0000000..48efa52 --- /dev/null +++ b/doc/Aerospike_cluster_discovery.txt @@ -0,0 +1,70 @@ +Pre-requisites: +1. a 'Cluster Seed': :; +2. Term 'connection' means 'TCP connection' in this document. + +Step 1.1. Establish connection to a 'Cluster Seed'; +Step 1.2. LOGIN to the 'Cluster Seed' providing 'User name' and 'Blowfish-encrypted password'; +Step 1.3. Request 'Access node' IP-address and Port from the 'Cluster Seed'; + +Step 2.1. Establish connection to the 'Access node'; +Step 2.2. LOGIN to the 'Access node' providing 'User name' and 'Blowfish-encrypted password'; +Step 2.3. Request list of cluster Node's IP-addresses and Ports; + +For each Node (IP-address and Port) acquired in Step 2.3. perform +Step 3.1. Establish connection to the 'Node'; +Step 3.2. LOGIN to the 'Node' providing 'User name' and 'Blowfish-encrypted password'; +Step 3.3. Request list of 'Namespaces' from the 'Node'. + +Each 'Namespace' information in the response to the request in Step 3.3. contains the following: +4.1. 'Namespace' name as an ASCII-string; +4.2. List of 'Replicas' supported by the 'Node' for this 'Namespace'. + +Each 'Replica' information from the item 4.2. contains the following: +5.1. Base64 encoding of the 'Partitions' bitmap represented by the 'Replica'. + + + + ========================================= + | Namespace 'Test' | + |---------------------------------------| + | K1 | + | K2 | + ========================================= + | + Assignment of keys to partitions + | + V + ========================================= + | | K1 | | | K2 | | | | + |---------------------------------------| + | P0 | P1 | P2 | P3 | P4 | P5 | P6 | P7 | + ========================================= + | + Assignment of partitions to replicas + | + ------------------------------------- + | | + V V +[====== Replica1 =============================] [============================= Replica2 ======] +================ =========== ================ =========== ================ ================ +| P0 | P1 | P2 | | P3 | P4 | | P5 | P6 | P7 | | P4 | P7 | | P0 | P3 | P6 | | P1 | P2 | P5 | +|--------------| |---------| |--------------| |---------| |--------------| |--------------| +| Node1 | | Node2 | | Node3 | | Node1 | | Node2 | | Node3 | +|--------------| |---------| |--------------| |---------| |--------------| |--------------| +| | K1 | | | | K2 | | | | | | K2 | | | | | | | K1 | | | +================ =========== ================ =========== ================ ================ + +Node1 response to 4.1., 4.2., 5.1.: + Test1: - Namespace name + Replica1: 11100000 - Node1 participates in partitions P0, P1, P2 for Replica1 + Replica2: 00001001 - Node1 participates in partitions P4, P7 for Replica2 + +Node2 response to 4.1., 4.2., 5.1.: + Test1: - Namespace name + Replica1: 00011000 - Node2 participates in partitions P3, P4 for Replica1 + Replica2: 10010010 - Node1 participates in partitions P0, P3, P6 for Replica2 + +Node3 response to 4.1., 4.2., 5.1.: + Test1: - Namespace name + Replica1: 00000111 - Node3 participates in partitions P5, P6, P7 for Replica1 + Replica2: 01100100 - Node1 participates in partitions P1, P2, P5 for Replica2 diff --git a/doc/Aerospike_packet_structure.txt b/doc/Aerospike_packet_structure.txt new file mode 100644 index 0000000..3ed318b --- /dev/null +++ b/doc/Aerospike_packet_structure.txt @@ -0,0 +1,574 @@ +Each packet sent over wire from a client to an Aerospike cluster node and back starts with packet header. + +PACKET HEADER + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| Proto Version | Packet Type | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + Total 8 bytes + +'Proto Version' = 2 +'Packet Type': +- INFO = 1; +- ADMIN = 2; +- MESSAGE = 3 (Key-value operations related); +- COMPRESSED = 4. +'Total body length': number of bytes following the Packet Header + + +ENCODINGS OF PARTS OF PACKETS THAT ARE GENERIC for ALL Packet Types. + +LENGTH-TAG-VALUE (LTV) ENCODING +'Tag' - 1 byte +'Value' - any number of bytes + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| Len = Length of the following Tag and Value | + +---------------+---------------+---------------+---------------+ + 4| Tag | Value, byte 0 | Value, byte 1 | Value, byte 2 | + +---------------+---------------+---------------+---------------+ + 8| Value, byte 3 | ... | byte (Len-2) | + +---------------+---------------+---------------+ + +LENGTH-VALUE (LV) ENCODING +'Value' - any number of bytes + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| Len = Length of the following Value | + +---------------+---------------+---------------+---------------+ + 4| Value, byte 0 | Value, byte 1 | Value, byte 2 | Value, byte 3 | + +---------------+---------------+---------------+---------------+ + 8| Value, byte 4 | ... | byte (Len-1) | + +---------------+---------------+---------------+ + + +Section. ADMIN Packet Type. + +LOGIN Request +'Packet Type': ADMIN = 2, 'Command': LOGIN=20(0x14) +Parameters: + USER - User's name; + CREDENTIAL - Blowfish (Aerospike-specific) Encrypted Password, 60 bytes. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 2 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 0 | 0 | Command = 20 | Field Count=2 | + +---------------+---------------+---------------+---------------+ + 12| 0 | 0 | 0 | 0 | + +---------------+---------------+---------------+---------------+ + 16| 0 | 0 | 0 | 0 | + +---------------+---------------+---------------+---------------+ + 20| 0 | 0 | 0 | 0 | + +---------------+---------------+---------------+---------------+ + 24| LTV, T=(CRED FldId=3), V=(Blowfish Encrypted Password) | + +---------------+---------------+---------------+---------------+ + ...| LTV encoding of Blowfish Encrypted Password (60 bytes) | + +---------------+---------------+---------------+---------------+ + 88| Pwd,last byte | LTV, T=(USER FldId=0), V=(User name) | + +---------------+---------------+---------------+---------------+ + 92| Len,last byte | LTV encoding of User name | + +---------------+---------------+---------------+---------------+ + ...| LTV encoding of User name | + +---------------+---------------+---------------+---------------+ + | User name, last bytes | + +---------------+---------------+ + Total bytes: + 24 (Header) + (4 + 1 + 60 (Password length)) + (4 + 1 + (Length of User name)) + +LOGIN Response +'Packet Type': ADMIN = 2 + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 2 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits | + +---------------+---------------+---------------+---------------+ + 8| Unused | Status | Unused |Field count (N)| + +---------------+---------------+---------------+---------------+ + 12| LTV_1 | + +---------------+---------------+---------------+---------------+ + ...| LTV_1 | LTV_2 | + +---------------+---------------+---------------+---------------+ + ...| LTV_2 | ... | + +---------------+---------------+---------------+---------------+ + ...| ... | LTV_N | + +---------------+---------------+---------------+---------------+ + ...| ... | LTV_N | + +---------------+---------------+---------------+ + Total bytes: + 12 (Header) + (4 + Len_1) + (4 + Len_2) + ... + (4 + Len_N) + \______________________________________/ + 'Field count' +'Status': + 'Status' = 0 is an indication of successful LOGIN; + For complete list of Status codes and related Status messages + see aspike_status.hrl, aspike_status.erl. +'Tag': + SESSION_TOKEN = 5 + SESSION_TTL = 6 + +Section. ADMIN Packet Type. End + +Section. INFO Packet Type + +INFO HEADER +'Packet Type': INFO = 1 + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 1 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits | + +---------------+---------------+---------------+---------------+ + +INFO Request, Parameters: List of predefined 'Names' +'Packet Type': INFO = 1 +Some (but not all) known 'Names': + - service-clear-std: to query a cluster 'seed' node for an 'access' node; + - node: to query Node ID; + - peers-clear-std: to query list of cluster nodes; + - namespaces: to query list of 'namespaces' available; + - namespace/: to query the parameters; + - partitions: to query a number of partitions; + - replicas: to query replicas information; + - best-practices; + - bins/[]; + - feature-key; + - get-config; + - get-config:context=namespace;id=; + - get-config:context=network; + - get-stats; + - health-outliers; + - health-stats; + - histogram:namespace=pi-stream;type=object-size; + - histogram:namespace=pi-stream;set=set-gateway;type=object-size; + - histogram:namespace=pi-stream;set=set-gateway;type=ttl; + - latencies:; + - latencies:hist={}-read; + - latencies:hist={}-write; + - log/0; + - logs; + - mesh. +For more details, see https://docs.aerospike.com/server/reference/info?version=6. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 1 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + binary concatenate the above header + with + Name1 appended with '\n' \ + Name2 appended with '\n' | + ... | + NameN appended with '\n' / + +INFO Response +'Packet Type': INFO = 1 +'Result code': + 'Result code' = 0 is an indication of successful GET; + For complete list of 'Result codes' and related messages + see aspike_status.hrl, aspike_status.erl. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 1 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + binary concatenated + with + 'Field' 1 appended with '\n' \ + 'Field' 2 appended with '\n' | + ... | + 'Field' N appended with '\n' / + where each 'Field' contains tab('\t')-separated values. + The content of each 'Field' is specific to the 'Name' sent in the INFO Request. + +Section. INFO Packet Type. End + +Section. MESSAGE Packet Type. + +ENCODING OF PARTS OF PACKETS THAT ARE GENERIC for MESSAGE Packet Types. + +MESSAGE HEADER +'Packet Type': MESSAGE = 3 + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + byte 8 = 22 - number of bytes in the MESSAGE (Packet Type) HEADER + Total bytes: + 30 bytes = 8 bytes (PACKET header) + 22 (MESSAGE header) + + +'Key digest' is RIPEMD-160 message digest of pair ('Set name', 'Key'). + + +('Namespace', 'Set name', 'Key digest' (RIPEMD-160)) ENCODING. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| LTV, T=(NAMESPACE FldId=0), V=(Namespace) | + +---------------+---------------+---------------+---------------+ + ...| LTV, T=(SETNAME FldId=1), V=(Set name) | + +---------------+---------------+---------------+---------------+ + ...| LTV, T=(DIGEST FldId=4), V=(Key digest (20 bytes)) | + +---------------+---------------+---------------+---------------+ + + +('Operation type', 'Bin name', 'Value type', 'Value') ENCODING. +'Operation type': + 1 - READ; + 2 - WRITE; + ... + 17; + +'Value type': + 0 - UNDEF, Value len = 0; + 1 - INTEGER, Value len = 8 bytes; + 2 - DOUBLE, Value len = 8 bytes; + 3 - STRING, Value len = length of the string, in bytes; + 4 - BLOB, Value len = length of the blob, in bytes; + 17 - BOOL, Value len = 1 byte, Value: 0 or 1; + +('Operation type', 'Bin name', 'Value type', 'Value') ENCODING is +the LENGTH-VALUE (LV) ENCODING where the VALUE is the packet below: + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| Operation type| Value type | 0 (Unused) | Bin name len | + +---------------+---------------+---------------+---------------+ + 4|Bin name,byte0 |Bin name,byte1 | ... | + +---------------+---------------+---------------+---------------+ + ...| Bin name, last byte | Value | + +---------------+---------------+---------------+---------------+ + ...| Value - number of allocated bytes depends on Value type | + +---------------+---------------+---------------+---------------+ + +('Bin name', 'Value type', 'Value') ENCODING. +('Bin name', 'Value type', 'Value') ENCODING is +the ('Operation type', 'Bin name', 'Value type', 'Value') ENCODING +where 'Operation type' is ignored (set to 0). + +('Operation type', 'Bin name') ENCODING is +the LENGTH-VALUE (LV) ENCODING where the VALUE is the packet below: + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| Operation type| 0 (Unused) | 0 (Unused) | Bin name len | + +---------------+---------------+---------------+---------------+ + 4|Bin name,byte0 |Bin name,byte1 | ... | + +---------------+---------------+---------------+---------------+ + ...| Bin name, last byte | + +---------------+---------------+ + + +PUT Request, Parameters: 'Namespace', 'Set name', 'Key digest', array of pairs ('Bin_name', 'Bin_value') +'Packet Type': MESSAGE = 3 +'Write attr' = 1 (AS_MSG_INFO2_WRITE) +'Field count' = 3 - Namespace, Set name, Key digest +'Operation' = 2 (AS_OPERATOR_WRITE) +'Bin count' = number of pairs in array of pairs ('Bin_name', 'Bin_value') + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + binary concatenate the above header + with ('Namespace', 'Set name', 'Key digest' (RIPEMD-160)) ENCODING + and + with + (2=AS_OPERATOR_WRITE, 'Bin name' 1, 'Value type' 1, 'Value' 1) ENCODING \ + (2=AS_OPERATOR_WRITE, 'Bin name' 2, 'Value type' 2, 'Value' 2) ENCODING | 'Bin count' + ... | + (2=AS_OPERATOR_WRITE, 'Bin name' N, 'Value type' N, 'Value' N) ENCODING / + +PUT Response +'Packet Type': MESSAGE = 3 +'Result code': + 'Result code' = 0 is an indication of successful PUT; + For complete list of 'Result codes' and related messages + see aspike_status.hrl, aspike_status.erl. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + + +GET Request, Parameters: 'Namespace', 'Set name', 'Key digest', array of 'Bin_names' +'Packet Type': MESSAGE = 3 +'Read attr' = 1 (AS_MSG_INFO1_READ - to read the bins, submitted as parameters) +'Read attr' = 3 (1 bitwise or 2) + (AS_MSG_INFO1_READ bitwise or AS_MSG_INFO1_GET_ALL - to read all bins for the submitted 'Key digest') +'Field count' = 3 - 'Namespace', 'Set name', 'Key digest' +'Operation' = 1 (AS_OPERATOR_READ) +'Bin count' = number of elements in array of 'Bin_names' + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + binary concatenate the above header + with ('Namespace', 'Set name', 'Key digest' (RIPEMD-160)) ENCODING + and, if 'Read attr' = 1 (AS_MSG_INFO1_READ - to read the bins, submitted as parameters), + with + (1=AS_OPERATOR_READ, 'Bin name' 1) ENCODING \ + (1=AS_OPERATOR_READ, 'Bin name' 2) ENCODING | 'Bin count' + ... | + (1=AS_OPERATOR_READ, 'Bin name' N) ENCODING / + +GET Response +'Packet Type': MESSAGE = 3 +'Result code': + 'Result code' = 0 is an indication of successful GET; + For complete list of 'Result codes' and related messages + see aspike_status.hrl, aspike_status.erl. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + binary concatenated + with + LV-ENCODING 'Field' 1 \ + LV-ENCODING 'Field' 2 | 'Field count' + ... | + LV-ENCODING 'Field' N / + and + with + ('Bin name' 1, 'Value type' 1, 'Value' 1) ENCODING \ + ('Bin name' 2, 'Value type' 2, 'Value' 2) ENCODING | 'Bin count' + ... | + ('Bin name' M, 'Value type' M, 'Value' M) ENCODING / + + +REMOVE Request, Parameters: 'Namespace', 'Set name', 'Key digest' +'Packet Type': MESSAGE = 3 +'Write attr' = 3 (1 bitwise or 2) + (AS_MSG_INFO2_WRITE bitwise or AS_MSG_INFO2_DELETE) +'Field count' = 3 - 'Namespace', 'Set name', 'Key digest' + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + binary concatenate the above header + with ('Namespace', 'Set name', 'Key digest' (RIPEMD-160)) ENCODING + +REMOVE Response +'Packet Type': MESSAGE = 3 +'Result code': + 'Result code' = 0 is an indication of successful REMOVE; + For complete list of 'Result codes' and related messages + see aspike_status.hrl, aspike_status.erl. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + + +EXISTS Request, Parameters: 'Namespace', 'Set name', 'Key digest' +'Packet Type': MESSAGE = 3 +'Read attr' = 33 (1 bitwise or 32) + (AS_MSG_INFO1_READ bitwise or AS_MSG_INFO1_GET_NOBINDATA) +'Field count' = 3 - 'Namespace', 'Set name', 'Key digest' + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + binary concatenate the above header + with ('Namespace', 'Set name', 'Key digest' (RIPEMD-160)) ENCODING + +EXISTS Response +'Packet Type': MESSAGE = 3 +'Result code': + 'Result code' = 0 is an indication of successful EXISTS; + For complete list of 'Result codes' and related messages + see aspike_status.hrl, aspike_status.erl. + + Byte/ 0 | 1 | 2 | 3 | + / | | | | + |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| + +---------------+---------------+---------------+---------------+ + 0| 2 | 3 | | + +---------------+---------------+---------------+---------------+ + 4| | Total body length (128 * 1024 * 1024, 27 bits) | + +---------------+---------------+---------------+---------------+ + 8| 22 | Read attr | Write attr | Info attr | + +---------------+---------------+---------------+---------------+ + 12| Unused | Result code | Generation (32-bits) | + +---------------+---------------+---------------+---------------+ + 16| Generation (32-bits) | TTL (32-bits) | + +---------------+---------------+---------------+---------------+ + 20| TTL (32-bits) | Timeout (32-bits) | + +---------------+---------------+---------------+---------------+ + 24| Timeout (32-bits) | Field count | + +---------------+---------------+---------------+---------------+ + 28| Bin count | + +---------------+---------------+ + +Section. MESSAGE Packet Type. End diff --git a/doc/Aerospike_protocol_encoders_decoders_naming_conventions.txt b/doc/Aerospike_protocol_encoders_decoders_naming_conventions.txt new file mode 100644 index 0000000..1ac4abf --- /dev/null +++ b/doc/Aerospike_protocol_encoders_decoders_naming_conventions.txt @@ -0,0 +1,217 @@ +Naming convention: + enc_XXX_request - encode XXX request + enc_XXX_response - encode response to XXX request + dec_XXX_request - decode XXX request + dec_XXX_response - decode response to XXX request + +Flow: + XXX request -> + -> enc_XXX_request -> + -> transport -> + -> dec_XXX_request -> + -> process XXX request to produce a response to the XXX request -> + -> enc_XXX_response -> + -> transport -> + -> dec_XXX_response -> + -> process the response to the XXX request. + +Use cases: +============================================== +| | _request | _response | +---------------------------------------------- +| enc_ | to use in Client | to use in Server | +| dec_ | to use in Server | to use in Client | +============================================== + +Decoders, dec_XXX, convention: +Return: + need_more + {error, Reason} + {ok, Decoded, Rest} + + +==================== +High-level requests + LOGIN + enc_login_request + dec_login_request + enc_login_response + dec_login_response + PUT + enc_put_request + dec_put_request + enc_put_response + dec_put_response + GET + enc_get_request + dec_get_request + enc_get_response + dec_get_response + +==================== +LOGIN + +enc_login_request + -> enc_login_request_pkt + -> enc_admin_header + -> <<...>> + -> enc_ltv + -> <<...>> + -> enc_admin_pkt + -> enc_proto + -> <<...>> + +dec_login_request + <- dec_admin_pkt + <- dec_proto + <- <<...>> + <- dec_login_request_pkt + <- dec_admin_header + <- <<...>> + <- dec_ltv + <- <<...>> + +enc_login_response + -> enc_proto_admin_fields + -> from_admin_field + -> enc_ltv + -> <<...>> + -> enc_login_response_pkt + -> <<...>> + -> enc_admin_pkt + -> enc_proto + -> <<...>> + +dec_login_response + <- dec_admin_pkt + <- dec_proto + <- <<...>> + <- dec_login_response_pkt + <- dec_proto_admin_fields + <- to_admin_field + <- dec_ltv + <- <<...>> + +==================== +PUT + +enc_put_request + -> enc_put_request_pkt + -> enc_put_header + -> enc_message_type_header + -> <<...>> + -> enc_key_digest + -> enc_ltv + -> <<...>> + -> <<...>> + -> enc_bins + -> enc_bin + -> to_typed_enc_value + -> enc_bin_typed_value + -> enc_lv + -> <<...>> + -> enc_message_pkt + -> enc_proto + -> <<...>> + +dec_put_request + <- dec_message_pkt + <- dec_proto + <- <<...>> + <- dec_put_request_pkt + <- dec_put_header + <- dec_message_type_header + <- <<...>> + <- dec_key_digest + <- dec_ltv + <- <<...>> + <- dec_bins + <- dec_bin + <- <<...>> + <- from_typed_enc_value + +enc_put_response + -> enc_put_response_pkt + -> enc_message_type_header + -> <<...>> + -> enc_message_pkt + -> enc_proto + -> <<...>> + +dec_put_response + <- dec_message_pkt + <- dec_proto + <- <<...>> + <- dec_put_response_pkt + <- dec_message_type_header + <- <<...>> + +==================== +GET + +enc_get_request + -> enc_get_request_pkt + -> enc_get_header + -> enc_message_type_header + -> <<...>> + -> enc_key_digest + -> enc_ltv + -> <<...>> + -> <<...>> + -> enc_bin_names + -> enc_bin_name + -> <<...>> + -> enc_message_pkt + -> enc_proto + -> <<...>> + +dec_get_request + <- dec_message_pkt + <- dec_proto + <- <<...>> + <- dec_get_request_pkt + <- dec_get_header + <- dec_message_type_header + <- <<...>> + <- check_get_request + <- dec_key_digest + <- dec_ltv + <- <<...>> + <- dec_bin_names + <- dec_bin_name + <- <<...>> + +enc_get_response + -> enc_get_response_pkt + -> enc_get_response_header + -> enc_message_type_header + -> <<...>> + -> enc_fields_and_ops + -> enc_fields + -> enc_lv + -> <<...>> + -> enc_ops_response + -> enc_op_response + -> to_typed_enc_value + -> <<...>> + -> <<...>> + -> enc_message_pkt + -> enc_proto + -> <<...>> + +dec_get_response + <- dec_message_pkt + <- dec_proto + <- <<...>> + <- dec_get_response_pkt + <- dec_message_type_header + <- <<...>> + <- dec_fields_and_ops + <- dec_lv + <- <<...>> + <- dec_ops + <- dec_lv + <- <<...>> + <- dec_op + <- from_typed_enc_value + <- <<...>> diff --git a/examples/exists_example.erl b/examples/exists_example.erl new file mode 100644 index 0000000..8ae4794 --- /dev/null +++ b/examples/exists_example.erl @@ -0,0 +1,31 @@ +-module(exists_example). + +%% API +-export([ + exists/7 +]). + +exists(Address, Port, User, Encrypted_password, Namespace, Set, Key) -> + case aspike_protocol:open_session(Address, Port, User, Encrypted_password) of + {error, Reason} -> + {error, {Reason, put, Address, Port, User}}; + {ok, #{socket := Socket} = Session} -> + Key_digest = aspike_protocol:digest(Set, Key), + Encoded = aspike_protocol:enc_exists_request(Namespace, Set, Key_digest), + ok = gen_tcp:send(Socket, Encoded), + case aspike_protocol:receive_response_message(Socket, 1000) of + {error, _Reason} = Err -> + aspike_protocol:close_session(Session), + Err; + Response -> + aspike_protocol:close_session(Session), + case aspike_protocol:dec_exists_response(Response) of + need_more -> + {error, <<"Not enough data to decode response">>}; + {error, _Reason} = Err -> Err; + {ok, Decoded, _Rest} -> + Result_code = Decoded, + aspike_status:status(Result_code) + end + end + end. diff --git a/examples/get_example.erl b/examples/get_example.erl new file mode 100644 index 0000000..d4159df --- /dev/null +++ b/examples/get_example.erl @@ -0,0 +1,33 @@ +-module(get_example). + +%% API +-export([ + get/8 +]). + +get(Address, Port, User, Encrypted_password, Namespace, Set, Key, Bins) -> + case aspike_protocol:open_session(Address, Port, User, Encrypted_password) of + {error, Reason} -> + {error, {Reason, put, Address, Port, User}}; + {ok, #{socket := Socket} = Session} -> + Key_digest = aspike_protocol:digest(Set, Key), + Encoded = aspike_protocol:enc_get_request(Namespace, Set, Key_digest, Bins), + ok = gen_tcp:send(Socket, Encoded), + case aspike_protocol:receive_response_message(Socket, 1000) of + {error, _Reason} = Err -> + aspike_protocol:close_session(Session), + Err; + Response -> + aspike_protocol:close_session(Session), + case aspike_protocol:dec_get_response(Response) of + need_more -> + {error, <<"Not enough data to decode response">>}; + {error, _Reason} = Err -> Err; + {ok, Decoded, _Rest} -> + {Result_code, Fields, Ops} = Decoded, + Status = aspike_status:status(Result_code), + io:format("~p~n", [Status]), + io:format("~p, ~p~n", [Fields, Ops]) + end + end + end. diff --git a/examples/info_example.erl b/examples/info_example.erl new file mode 100644 index 0000000..1413342 --- /dev/null +++ b/examples/info_example.erl @@ -0,0 +1,29 @@ +-module(info_example). + +%% API +-export([ + info/6 +]). + +info(Address, Port, User, Encrypted_password, Names, Timeout) -> + case aspike_protocol:open_session(Address, Port, User, Encrypted_password) of + {error, Reason} -> + {error, {Reason, info, Address, Port, User}}; + {ok, #{socket := Socket} = Session} -> + Encoded = aspike_protocol:enc_info_request(Names), + ok = gen_tcp:send(Socket, Encoded), + case aspike_protocol:receive_response_info(Socket, Timeout) of + {error, _Reason} = Err -> + aspike_protocol:close_session(Session), + Err; + Response -> + aspike_protocol:close_session(Session), + case aspike_protocol:dec_info_response(Response) of + need_more -> + {error, <<"Not enough data to decode response">>}; + {error, _Reason} = Err -> Err; + {ok, Decoded, _Rest} -> + Decoded + end + end + end. diff --git a/examples/login_example.erl b/examples/login_example.erl new file mode 100644 index 0000000..7f175e9 --- /dev/null +++ b/examples/login_example.erl @@ -0,0 +1,17 @@ +-module(login_example). + +%% API +-export([login/4]). + +login(Address, Port, User, Password) -> + % NOTE: aspike_blowfish:crypt could take 2-5 seconds + io:format("[login] encrypting password (could take 2-5 seconds)..."), + Encrypted_password = aspike_blowfish:crypt(Password), + io:format(" ok~n"), + case aspike_protocol:open_session(Address, Port, User, Encrypted_password) of + {error, Reason} -> + io:format("~p:~p, User: ~p: failed to login: ~p~n", [Address, Port, User, Reason]); + {ok, #{address := A, port := P, user := U, socket:= _S} = Session} -> + io:format("~p:~p, User: ~p: logged in.~n", [A, P, U]), + aspike_protocol:close_session(Session) + end. diff --git a/examples/put_example.erl b/examples/put_example.erl new file mode 100644 index 0000000..499884d --- /dev/null +++ b/examples/put_example.erl @@ -0,0 +1,31 @@ +-module(put_example). + +%% API +-export([ + put/8 +]). + +put(Address, Port, User, Encrypted_password, Namespace, Set, Key, Bins_Values) -> + case aspike_protocol:open_session(Address, Port, User, Encrypted_password) of + {error, Reason} -> + {error, {Reason, put, Address, Port, User}}; + {ok, #{socket := Socket} = Session} -> + Key_digest = aspike_protocol:digest(Set, Key), + Encoded = aspike_protocol:enc_put_request(Namespace, Set, Key_digest, Bins_Values), + ok = gen_tcp:send(Socket, Encoded), + case aspike_protocol:receive_response_message(Socket, 1000) of + {error, _Reason} = Err -> + aspike_protocol:close_session(Session), + Err; + Response -> + aspike_protocol:close_session(Session), + case aspike_protocol:dec_put_response(Response) of + need_more -> + {error, <<"Not enough data to decode response">>}; + {error, _Reason} = Err -> Err; + {ok, Decoded, _Rest} -> + Result_code = Decoded, + aspike_status:status(Result_code) + end + end + end. diff --git a/examples/remove_example.erl b/examples/remove_example.erl new file mode 100644 index 0000000..2b829ec --- /dev/null +++ b/examples/remove_example.erl @@ -0,0 +1,31 @@ +-module(remove_example). + +%% API +-export([ + remove/7 +]). + +remove(Address, Port, User, Encrypted_password, Namespace, Set, Key) -> + case aspike_protocol:open_session(Address, Port, User, Encrypted_password) of + {error, Reason} -> + {error, {Reason, put, Address, Port, User}}; + {ok, #{socket := Socket} = Session} -> + Key_digest = aspike_protocol:digest(Set, Key), + Encoded = aspike_protocol:enc_remove_request(Namespace, Set, Key_digest), + ok = gen_tcp:send(Socket, Encoded), + case aspike_protocol:receive_response_message(Socket, 1000) of + {error, _Reason} = Err -> + aspike_protocol:close_session(Session), + Err; + Response -> + aspike_protocol:close_session(Session), + case aspike_protocol:dec_remove_response(Response) of + need_more -> + {error, <<"Not enough data to decode response">>}; + {error, _Reason} = Err -> Err; + {ok, Decoded, _Rest} -> + Result_code = Decoded, + aspike_status:status(Result_code) + end + end + end. diff --git a/include/aspike.hrl b/include/aspike.hrl new file mode 100644 index 0000000..8ec7fe2 --- /dev/null +++ b/include/aspike.hrl @@ -0,0 +1,61 @@ +-record(aspike_endpoint, { + name :: string(), + address :: inet:ip_address() | inet:hostname(), + port :: inet:port_number() +}). + +-record(aspike_user, { + name :: binary(), + credential :: aspike:credential() +}). + +-record(aspike_connection_options, { + reconnect :: boolean(), + reconnect_time_max :: pos_integer() | infinity, + reconnect_time_min :: pos_integer(), + socket_options :: [gen_tcp:connect_option()] +}). + +-record(aspike_pool_options, { + backlog_size :: pos_integer() | infinity, + max_retries :: non_neg_integer(), + pool_size :: pos_integer(), + pool_strategy :: random | round_robin +}). + +-record(aspike_node_params, { + endpoint :: #aspike_endpoint{}, + user :: #aspike_user{}, + connection_options :: #aspike_connection_options{}, + pool_options :: #aspike_pool_options{} +}). + +-type aspike_node_config_item() :: + {node, string()} | + {address, string()} | + {port, non_neg_integer()} | + {user, string()} | + {password, string()} | + {password_file, string()} | + {reconnect, true | false} | + {reconnect_time_max, non_neg_integer()} | + {reconnect_time_min, non_neg_integer()} | + {backlog_size, non_neg_integer()} | + {max_retries, non_neg_integer()} | + {pool_size, non_neg_integer()} | + {pool_strategy, random | round_robin}. + +-type aspike_node_flat_config() :: [aspike_node_config_item()]. + +-define(DEFAULT_NODE, "A777"). +-define(DEFAULT_ADDRESS, "127.0.0.1"). +-define(DEFAULT_PORT, 54321). +-define(DEFAULT_USER, "user_not_provided"). +-define(DEFAULT_PASSWORD, "password_not_provided"). +-define(DEFAULT_RECONNECT, false). +-define(DEFAULT_RECONNECT_TIME_MAX, 120000). +-define(DEFAULT_RECONNECT_TIME_MIN, 2000). +-define(DEFAULT_BACKLOG_SIZE, 1024). +-define(DEFAULT_MAX_RETRIES, 0). +-define(DEFAULT_POOL_SIZE, 1). +-define(DEFAULT_POOL_STRATEGY, random). diff --git a/src/aspike.erl b/src/aspike.erl new file mode 100644 index 0000000..43a0fef --- /dev/null +++ b/src/aspike.erl @@ -0,0 +1,74 @@ +-module(aspike). +-include("../include/aspike.hrl"). +-include("../include/aspike_protocol.hrl"). + +%% API +-export_type([ + node_id/0, + node_params/0, + namespace/0, + set/0, + key_digest/0, + credential/0, + bin_name/0, + bin_value/0, + bin/0, + bins/0, + bin_names/0, + op/0, + response_id/0, + response/0, + handled_response/0, + status/0, + status_code/0, + as_proto/0, + as_msg/0, + uint8_t/0, + uint16_t/0, + uint32_t/0, + uint48_t/0 +]). + +-type node_id() :: atom(). +-type node_params() :: #aspike_node_params{}. +-type namespace() :: string(). +-type set() :: string(). +-type key_digest() :: <<_:(20*8)>>. % RIPEMD-160 digest of key for Aerospike record +-type credential() :: <<_:(60*8)>>. % Blowfish ciphered password, BUT with Aerospike specifics +-type bin_name() :: string(). +-type bin_value() :: undefined | boolean() | + integer() | float() | + string() | binary(). +-type bin() :: {bin_name(), bin_value()}. +-type bins() :: [bin()]. +-type bin_names() :: [bin_name()]. +-type op() :: put | get | remove | exists. + +-type uint8_t() :: 0..255. +-type uint16_t() :: 0..65_535. +-type uint32_t() :: 0..4_294_967_295. +%%-type uint48_t() :: 0..281_474_976_710_655. +-type uint48_t() :: 0..(?PROTO_SIZE_MAX-1). % 28 bits out of 48 + +-type as_proto() :: #as_proto{}. +-type as_msg() :: #as_msg{}. + +-type response_field() :: binary(). % <<_:0..4_294_967_295>>. +-type response_fields() :: [response_field()]. +-type response_bin_name() :: binary(). +-type response_op() :: {response_bin_name(), bin_value()}. +-type response_ops() :: [response_op()]. +%%-type response_id() :: shackle:request_id() :: {shackle_server:name(), reference()} :: {atom(), reference()}. +-type response_id() :: {atom(), reference()}. +-type response() :: {as_proto(), as_msg(), response_fields(), response_ops()}. +-type handled_response() :: ok | boolean() | {ok, bins()} | + {error, { + record_not_found | + aspike:status() | + {aspike:op(), term()} | + {unrecognized_op_response, aspike:op(), term()}}}. + +-type status_code() :: uint8_t(). +-type status_code_string() :: binary(). +-type status_msg() :: binary(). +-type status() :: {status_code(), status_code_string(), status_msg()}. \ No newline at end of file diff --git a/src/aspike_protocol.app.src b/src/aspike_protocol.app.src index 9b36366..2726dc6 100644 --- a/src/aspike_protocol.app.src +++ b/src/aspike_protocol.app.src @@ -1,5 +1,5 @@ {application, aspike_protocol, - [{description, "An OTP library"}, + [{description, "Implementation of Aerospike binary protocol in Erlang"}, {vsn, "0.1.0"}, {registered, []}, {applications, diff --git a/src/aspike_protocol.erl b/src/aspike_protocol.erl index 0c40863..7f13fcb 100644 --- a/src/aspike_protocol.erl +++ b/src/aspike_protocol.erl @@ -20,11 +20,15 @@ enc_remove_request/3, dec_remove_response/1, enc_exists_request/3, - dec_exists_response/1 + dec_exists_response/1, + % https://aerospike.com/docs/server/reference/info?version=7&all=true + enc_info_request/1, + dec_info_response/1 ]). %% High-level encoders/decoders, Server side -export([ + enc_error_response/1, dec_login_request/1, enc_login_response/2, dec_put_request/1, @@ -34,7 +38,9 @@ dec_remove_request/1, enc_remove_response/1, dec_exists_request/1, - enc_exists_response/1 + enc_exists_response/1, + dec_info_request/1, + enc_info_response/1 ]). %% Login-specific encoders/decoders @@ -105,6 +111,23 @@ dec_exists_header/1 ]). +%% Info-specific encoders/decoders +-export([ + enc_info_request_pkt/1, + dec_info_request_pkt/1, + enc_info_response_pkt/1, + dec_info_response_pkt/1 +]). + +%% Info-specific response parsers +-export([ + info_number_of_partitions/1, + info_replicas/1, + info_dec_partitions_bitmap/2, + info_bitmap_to_map/3, + info_bitmap_to_set/3 +]). + %% Key-specific -export([ digest/2, @@ -114,10 +137,13 @@ %% Protocol encoders/decoders -export([ + enc_error_response_pkt/1, enc_admin_pkt/1, dec_admin_pkt/1, enc_message_pkt/1, dec_message_pkt/1, + enc_info_pkt/1, + dec_info_pkt/1, enc_message_type_header/10, dec_message_type_header/1, enc_proto/2, @@ -141,9 +167,24 @@ dec_responses/1 ]). +%% Connect and Login utils +-export([ + open_session/4, + open_session/5, + close_session/1 +]). + +%% Receive response utils +-export([ + receive_response_info/2, + receive_response_message/2 +]). + %% High-level encoders/decoders, Client side +enc_login_request(User, {_Type, _Value} = Credential) -> + enc_admin_pkt(enc_login_request_pkt(User, Credential)); enc_login_request(User, Credential) -> - enc_admin_pkt(enc_login_request_pkt(User, Credential)). + enc_login_request(User, {?CREDENTIAL, Credential}). dec_login_response(Data) -> case dec_admin_pkt(Data) of @@ -166,7 +207,7 @@ dec_put_response(Data) -> {ok, {_Version, _Type, Data1}, Rest} -> case dec_put_response_pkt(Data1) of need_more -> need_more; - {ok, #aspike_message_type_header{} = Decoded, _Data2} -> + {ok, #aspike_message_type_header{result_code = Decoded}, _Data2} -> {ok, Decoded, Rest} end end. @@ -196,7 +237,7 @@ dec_remove_response(Data) -> {ok, {_Version, _Type, Data1}, Rest} -> case dec_remove_response_pkt(Data1) of need_more -> need_more; - {ok, #aspike_message_type_header{} = Decoded, _Data2} -> + {ok, #aspike_message_type_header{result_code = Decoded}, _Data2} -> {ok, Decoded, Rest} end end. @@ -211,14 +252,29 @@ dec_exists_response(Data) -> {ok, {_Version, _Type, Data1}, Rest} -> case dec_exists_response_pkt(Data1) of need_more -> need_more; - {ok, #aspike_message_type_header{} = Decoded, _Data2} -> + {ok, #aspike_message_type_header{result_code = Decoded}, _Data2} -> {ok, Decoded, Rest} end end. +enc_info_request(Names) -> + enc_info_pkt(enc_info_request_pkt(Names)). + +dec_info_response(Data) -> + case dec_info_pkt(Data) of + need_more -> need_more; + {error, _Reason} = Err -> Err; + {ok, {_Version, _Type, Data1}, Rest} -> + Decoded = dec_info_response_pkt(Data1), + {ok, Decoded, Rest} + end. + %% High-level encoders/decoders, Client side. End %% High-level encoders/decoders, Server side +enc_error_response(Status) -> + enc_message_pkt(enc_error_response_pkt(Status)). + dec_login_request(Data) -> case dec_admin_pkt(Data) of need_more -> need_more; @@ -306,6 +362,18 @@ dec_exists_request(Data) -> enc_exists_response(Status) -> enc_message_pkt(enc_exists_response_pkt(Status)). +dec_info_request(Data) -> + case dec_info_pkt(Data) of + need_more -> need_more; + {error, _Reason} = Err -> Err; + {ok, {_Version, _Type, Data1}, Rest} -> + Decoded = dec_info_request_pkt(Data1), + {ok, Decoded, Rest} + end. + +enc_info_response(Fields) -> + enc_info_pkt(enc_info_response_pkt(Fields)). + %% High-level encoders/decoders, Server side. End %% Login-specific encoders/decoders @@ -841,6 +909,93 @@ dec_exists_header(Data) -> %% Exists-specific encoders/decoders. End +%% Info-specific encoders/decoders +enc_info_request_pkt(Names) -> + lists:foldl(fun append_with_nl/2, <<>>, Names). + +append_with_nl(Suffix, Prefix) when is_binary(Suffix) -> + <>; +append_with_nl(Suffix, Prefix) when is_list(Suffix) -> + B = list_to_binary(Suffix), + <>. + +dec_info_request_pkt(Data) -> + binary:split(Data, [<<"\n">>], [global]). + +enc_info_response_pkt(Fields) -> + lists:foldl(fun (Field, Acc) -> + B = list_to_binary(lists:join("\t", Field)), + <> end, + <<>>, Fields). + +dec_info_response_pkt(Data) -> + Fields = binary:split(Data, [<<"\n">>], [global]), + lists:map(fun (X) -> + list_to_tuple(binary:split(X, [<<"\t">>], [global])) end, + Fields). + +%% Info-specific encoders/decoders. End + +%% Info-specific response parsers +info_number_of_partitions(Data) -> + aspike_util:binary_to_integer(Data). + +info_replicas(Data) -> + By_namespaces = binary:split(Data, [<<";">>], [global]), + [info_namespace_replicas(Ns) || Ns <- By_namespaces]. + +info_namespace_replicas(Data) -> + case binary:split(Data, [<<":">>]) of + [Namespace] -> + {Namespace, + {_N_regime = undefined, _N_replication_factor = undefined, _Bitmaps = []}}; + [Namespace, Rest] -> + Params = info_namespace_replicas_params(Rest), + {Namespace, + {_N_regime, _N_replication_factor, _Bitmaps} = Params} + end. + +info_namespace_replicas_params(Data) when is_binary(Data) -> + Parts = binary:split(Data, [<<",">>], [global]), + info_namespace_replicas_params(Parts); +info_namespace_replicas_params([Regime, Replication_factor | Bitmaps]) -> + N_regime = case aspike_util:binary_to_integer(Regime) of + {ok, R} -> R; + R_error -> R_error + end, + N_replication_factor = case aspike_util:binary_to_integer(Replication_factor) of + {ok, F} -> F; + F_error -> F_error + end, + {N_regime, N_replication_factor, Bitmaps}. + +info_dec_partitions_bitmap(N_partitions, Base64_encoded_bitmap) -> + Bitmap_size_needed = aspike_util:bitmap_size(N_partitions), + Expected_base64_encoding_len = aspike_util:base64_encoding_len(Bitmap_size_needed), + Actual_size = size(Base64_encoded_bitmap), + case Actual_size =:= Expected_base64_encoding_len of + true -> base64:decode(Base64_encoded_bitmap); + false -> {error, + {expected_encoded_bitmap_len, Expected_base64_encoding_len, + actual_encoded_bitmap_len, Actual_size}} + end. + +info_bitmap_to_map(<<>>, _Index, #{} = Acc) -> + Acc; +info_bitmap_to_map(<<2#0:1, Rest/bitstring>>, Index, #{} = Acc) -> + info_bitmap_to_map(Rest, Index + 1, Acc); +info_bitmap_to_map(<<2#1:1, Rest/bitstring>>, Index, #{} = Acc) -> + info_bitmap_to_map(Rest, Index + 1, Acc#{Index => 1}). + +info_bitmap_to_set(<<>>, _Index, Acc) -> + Acc; +info_bitmap_to_set(<<2#0:1, Rest/bitstring>>, Index, Acc) -> + info_bitmap_to_set(Rest, Index + 1, Acc); +info_bitmap_to_set(<<2#1:1, Rest/bitstring>>, Index, Acc) -> + info_bitmap_to_set(Rest, Index + 1, sets:add_element(Index, Acc)). + +%% Info-specific response parsers. End + %% Key-specific digest([], Key) -> K = enc_key(Key), @@ -893,6 +1048,19 @@ dec_key_digest(Data) -> %% Key-specific. End %% Protocol encoders/decoders +enc_error_response_pkt(Status) -> + enc_message_type_header( + Status, + _N_fields = 0, + _N_bins = 0, + _Ttl = 0, + _Timeout = 0, + _Read_attr = 0, + _Write_attr = 0, + _Info_attr = 0, + _Generation = 0, + _Unused = 0). + enc_admin_pkt(Data) -> enc_proto(?AS_ADMIN_MESSAGE_TYPE, Data). @@ -905,6 +1073,12 @@ enc_message_pkt(Data) -> dec_message_pkt(Data) -> dec_proto(?AS_MESSAGE_TYPE, Data). +enc_info_pkt(Data) -> + enc_proto(?AS_INFO_MESSAGE_TYPE, Data). + +dec_info_pkt(Data) -> + dec_proto(?AS_INFO_MESSAGE_TYPE, Data). + enc_message_type_header( % corresponds to as_command_write_header_write Result_code, N_fields, @@ -1100,3 +1274,89 @@ dec_responses(Data, Acc) -> %% {Acc, Data}. %% Response decoder for 'shackle client' handle_data/2. End + +%% Connect and login utils +open_session(Address, Port, User, Encrypted_password) -> + open_session(Address, Port, User, Encrypted_password, 1000). +open_session(Address, Port, User, Encrypted_password, Timeout) -> + case connect(Address, Port, Timeout) of + {error, _} = Err -> Err; + {ok, Socket} -> + Auth = authenticate(Socket, User, Encrypted_password), + case handle_authentication(Auth) of + {error, _Reason} = Err -> + disconnect(Socket), + Err; + {ok, #{} = Session} -> + {ok, Session#{ + address => Address, port => Port, user => User, + socket => Socket}} + end + end. + +close_session(#{socket := Socket}) -> + disconnect(Socket); +close_session(_) -> ok. + +connect(Address, Port, Timeout) when is_binary(Address) -> + connect(binary_to_list(Address), Port, Timeout); +connect(Address, Port, Timeout) -> + connect(Address, Port, [binary, {packet, raw}, {active, false}], Timeout). +connect(Address, Port, Opt, Timeout) -> + gen_tcp:connect(Address, Port, Opt, Timeout). + +disconnect(Socket) -> + gen_tcp:close(Socket). + +authenticate(Socket, User, Encrypted_password) -> + Login = aspike_protocol:enc_login_request(User, Encrypted_password), + ok = gen_tcp:send(Socket, Login), + Response = receive_response_login(Socket, 1000), + aspike_protocol:dec_login_response(Response). + +handle_authentication({ok, {?AEROSPIKE_OK, Fields}, Rest}) -> + authenticated(Fields, Rest); +handle_authentication({ok, {?AEROSPIKE_SECURITY_NOT_SUPPORTED, Fields}, Rest}) -> + authenticated(Fields, Rest); +handle_authentication({ok, {?AEROSPIKE_SECURITY_NOT_ENABLED, Fields}, Rest}) -> + authenticated(Fields, Rest); +handle_authentication({ok, {Status, Fields}, _Rest}) -> + Err = {aspike_status:status(Status), Fields}, + {error, Err}; +handle_authentication({error, Reason}) -> + {error, Reason}. + +authenticated(Fields, Rest) -> + Timestamp = erlang:system_time(second), + Token = proplists:get_value(session_token, Fields), + Ttl = proplists:get_value(session_ttl, Fields), + {ok, #{ts => Timestamp, token => Token, ttl => Ttl, + rest => Rest}}. + +%% Connect and login utils. End + +%% Receive response utils +receive_response_login(Socket, Timeout) -> + receive_proto(Socket, 8, Timeout, ?AS_ADMIN_MESSAGE_TYPE). + +receive_response_info(Socket, Timeout) -> + receive_proto(Socket, 8, Timeout, ?AS_INFO_MESSAGE_TYPE). + +receive_response_message(Socket, Timeout) -> + receive_proto(Socket, 8, Timeout, ?AS_MESSAGE_TYPE). + +receive_proto(Socket, Header_size, Timeout, Type) -> + Ret_header = gen_tcp:recv(Socket, Header_size, Timeout), + case Ret_header of + {ok, Header} -> + <> + = Header, + Ret_data = gen_tcp:recv(Socket, Size, Timeout), + case Ret_data of + {ok, Data} -> <
>; + Other_data -> Other_data + end; + Other_header -> Other_header + end. + +%% Receive response utils. End diff --git a/src/aspike_util.erl b/src/aspike_util.erl new file mode 100644 index 0000000..6221029 --- /dev/null +++ b/src/aspike_util.erl @@ -0,0 +1,122 @@ +-module(aspike_util). + +%% API +-export([ + binary_to_integer/1, + bitmap_size/1, + base64_encoding_len/1, + to_term/1 +]). + +binary_to_integer(Data) when is_binary(Data) -> + try erlang:binary_to_integer(Data) of + V -> {ok, V} + catch error:badarg -> + {error, {not_an_integer, Data}} + end; +binary_to_integer(Data) -> + {error, {not_a_binary, Data}}. + +%% Size of bit map, in bytes, that is needed to map N_partitions partitions +bitmap_size(N_partitions) -> + trunc((N_partitions + 7) / 8). + +%% Length, in bytes, that is needed to make base64 encoding of N_bytes +base64_encoding_len(N_bytes) -> + trunc((N_bytes+2)/3) bsl 2. + +%% Convert a string representation of a list to Erlang term +to_term(Str) -> + tokenized_to_term(tokenize(Str)). + +tokenized_to_term(Ts) -> + tokenized_to_term(Ts, []). + +-define(CHAR_COMMA, $,). +-define(CHAR_LSB, $[). +-define(CHAR_RSB, $]). + +tokenized_to_term([], [Stack]) -> + Stack; + +tokenized_to_term([sb_close|T], Stack) -> + Folded = fold_sb(Stack, []), + tokenized_to_term(T, Folded); +tokenized_to_term([quote_close|T], Stack) -> + Folded = fold_quote(Stack, []), + tokenized_to_term(T, Folded); +tokenized_to_term([?CHAR_COMMA|T], Stack) -> + tokenized_to_term(T, Stack); +tokenized_to_term([H|T], Stack) -> + tokenized_to_term(T, [H|Stack]). + +fold_sb([], []) -> + []; +fold_sb([], _Acc) -> + {error, missing_open_square_bracket}; +fold_sb([sb_open|T], Acc) -> + [Acc|T]; +fold_sb([H|T], Acc) -> + fold_sb(T, [H|Acc]). + +fold_quote([], []) -> + []; +fold_quote([], _Acc) -> + {error, missing_open_quote}; +fold_quote([quote_open|T], Acc) -> + [Acc|T]; +fold_quote([H|T], Acc) -> + fold_quote(T, [H|Acc]). + +quote_open(Acc) -> + [quote_open|Acc]. +quote_close(Acc) -> + [quote_close|Acc]. +quote_open_close(Acc) -> + quote_close(quote_open(Acc)). + +sb_open(Acc) -> + [sb_open|Acc]. +sb_close(Acc) -> + [sb_close|Acc]. + +tokenize([?CHAR_LSB|_]=Str) -> + tokenize(Str, []); +tokenize(Str) -> + tokenize(lists:append(["[", Str, "]"]), []). + +tokenize([], Acc) -> + lists:reverse(Acc); + +tokenize([?CHAR_LSB|T], [?CHAR_COMMA|_]=Acc) -> + tokenize(T, sb_open(Acc)); +tokenize([?CHAR_LSB|T], [sb_open|_]=Acc) -> + tokenize(T, sb_open(Acc)); +tokenize([?CHAR_LSB|T], Acc) -> + tokenize(T, sb_open(Acc)); +tokenize([?CHAR_COMMA|T], [sb_close|_]=Acc) -> + tokenize(T, [?CHAR_COMMA|Acc]); +tokenize([?CHAR_RSB|T], [?CHAR_COMMA|_]=Acc) -> + tokenize(T, sb_close(quote_open_close(Acc))); +tokenize([?CHAR_COMMA|T], [sb_open|_]=Acc) -> + tokenize(T, [?CHAR_COMMA|quote_open_close(Acc)]); +tokenize([?CHAR_COMMA|T], [?CHAR_COMMA|_]=Acc) -> + tokenize(T, [?CHAR_COMMA|quote_open_close(Acc)]); +tokenize([?CHAR_COMMA|T], Acc) -> + tokenize(T, [?CHAR_COMMA|quote_close(Acc)]); +tokenize([H|T], [?CHAR_COMMA|_]=Acc) -> + tokenize(T, [H|quote_open(Acc)]); + +tokenize([?CHAR_RSB|T], [sb_open|_]=Acc) -> + tokenize(T, sb_close(Acc)); +tokenize([?CHAR_RSB|T], [sb_close|_]=Acc) -> + tokenize(T, sb_close(Acc)); +tokenize([H|T], [sb_open|_]=Acc) -> + tokenize(T, [H|quote_open(Acc)]); +tokenize([?CHAR_RSB|T], Acc) -> + tokenize(T, sb_close(quote_close(Acc))); + +tokenize([H|T], Acc) -> + tokenize(T, [H|Acc]). + +%% Convert a string representation of a list to Erlang term. End diff --git a/test/aspike_protocol_test_enc_dec.erl b/test/aspike_protocol_test_enc_dec.erl index b1db259..961c842 100644 --- a/test/aspike_protocol_test_enc_dec.erl +++ b/test/aspike_protocol_test_enc_dec.erl @@ -21,6 +21,7 @@ aspike_protocol_enc_dec_test_() -> fun test_admin_pkt/0, fun test_admin_header/0, fun test_login_pkt/0, + fun test_info_pkt/0, fun test_login_request/0, fun test_login_response/0, fun test_dec_login_request/0, @@ -178,6 +179,24 @@ test_login_pkt() -> ?assertEqual(User, proplists:get_value(?USER, Fields)), ?assertEqual(Credential, proplists:get_value(?CREDENTIAL, Fields)). +test_info_pkt() -> + Names = ["Name1", "Name2", "Name3"], + Enc_request_pkt = aspike_protocol:enc_info_request_pkt(Names), + Dec_request_pkt = aspike_protocol:dec_info_request_pkt(Enc_request_pkt), + Names_expected = lists:append(lists:map(fun list_to_binary/1, Names), [<<>>]), + ?assertEqual(Names_expected, Dec_request_pkt), + + Fields = [ + ["Field1", "value1_1"], + ["Field2", "value2_1", "value2_2", "value2_3"], + ["Field3", "value3_1", "value3_2"]], + Enc_response_pkt = aspike_protocol:enc_info_response_pkt(Fields), + Dec_response_pkt = aspike_protocol:dec_info_response_pkt(Enc_response_pkt), + Fields_expected = lists:map(fun list_to_tuple/1, + lists:append(lists:map(fun (Field) -> + lists:map(fun list_to_binary/1, Field) end, Fields), [[<<>>]])), + ?assertEqual(Fields_expected, Dec_response_pkt). + test_login_request() -> User = <<"User">>, Credential = <<"Credential">>, Enc = aspike_protocol:enc_login_request(User, {?CREDENTIAL, Credential}), @@ -447,9 +466,9 @@ test_dec_put_request() -> test_dec_put_response() -> Enc = aspike_protocol:enc_put_response(?AEROSPIKE_OK), - {ok, #aspike_message_type_header{result_code = Result_code} = _Decoded, Rest} + {ok, Decoded, Rest} = aspike_protocol:dec_put_response(Enc), - ?assertEqual(?AEROSPIKE_OK, Result_code), + ?assertEqual(?AEROSPIKE_OK, Decoded), ?assertEqual(<<>>, Rest). test_get_request() ->