MP5
Wallet
Due dates may be found on the schedule.

A common component of many applications, from banking software to resource-based games like Cookie Clicker, is a wallet that holds a collection of resources. Users can interact the wallet by adding or subtracting resources from it, but no resource can ever go negative – debt is not allowed!

You’ll implement such a wallet in this MP, along with a multi-threaded server that uses a simple protocol to interact with your wallet over the network.

1 Initial Files

In your CS 340 directory, merge the initial starting files with the following commands:

git fetch release
git merge release/mp5 --allow-unrelated-histories -m "Merging initial files"

2 MP Requirements (Parts 1–3) - Wallet Library

In wallet.py, you may only import threading, no other libraries; and you may not use any global variables.

2.1 Part 1: The Wallet

For the first two parts of this MP, you will be working in the file wallet.py. You will implement the wallet library, which has only a few basic requirements:

  1. Your wallet initially starts empty, with 0 of all resources.
  2. Your wallet can accept any resource (identified by any string). The user of your library will call wallet_change and supply a delta to change your resource by a certain delta. You may assume that every delta is an int.
  3. You must NEVER let any resource in the wallet go negative. You must NOT return from wallet_change until you can satisfy the request (another thread must add to your wallet).
  4. You may have multiple wallets at once. All state of your wallet must be maintained in your class Wallet, not in global variables or the like.
  5. Finally, and most importantly, many threads will be accessing your wallet(s) at the same time. You must ensure the data in your wallet is correct by ensuring accesses to your wallet are properly synchronized.

The starter code has a simple class Wallet for you to extend with empty method bodies for you to implement.

2.2 Part 2: both blocking and non-blocking methods

change must block if there are not enough resources to be immediately satisfied. This must move the thread to a blocked state using one or more of the synchronization tools from the threading library, such as locks and conditional variables.

Two prohibited alternatives to blocking

Your wallet must NOT busy wait and must NOT sleep wait. Both busy and sleep waiting causes your CPU to make unnecessary checks and, in the case of sleeping, possibly waits an unnecessary extra amount of time.

# Example of "busy waiting" (wasting CPU cycles), which is NOT allowed:
while resource_count < delta:
    pass # do nothing 
# Example of "sleep waiting" (wasting CPU cycles or time, or a bit of both), which is NOT allowed:
while resource_count < delta:
    time.sleep(0.001)

try_change must not block, but also must not make resources negative. If change would block, try_change will instead immediately return False.

2.3 Part 3: multi-resource transactions

Sometimes we want to perform a multi-resource transaction as a single atomic operation. For example, if a group of friends agree to split the bill at a restaurant we only want to perform the transaction if every friend has enough money for their part of the bill; otherwise, we shouldn’t get any food.

The transaction method takes multiple keyword arguments, each with a resource name as a the keyword and the delta change to that resource as the value. It performs all of these changes at once, blocking if needed until they can be done in one action. You may assume that every delta is an int.

For example, if Luther, Wade, and Carl agreed to split a $45 restaurant bill we might run wallet.transaction(Luther=-15, Wade=-15, Carl=-15, Restaurant=+45); this would block until all three of Luther, Wade, and Carl have at least 15 in their accounts and then reduce all three by 15 and increase the restaurant by 45 all at once, not allowing any other threads to do anything else to these four accounts in between the various -15 and +45 operations.

Synchronize each resource or each wallet?

You can design your wallet so that each resource has its own synchronization primitive to use in blocking, or so that there is just one single synchronization primitive shared by all resources in the wallet. There are pros and cons to each.

Being able to synchronize each resource individually provides fine-grained control and scales up well to large numbers of threads and resources accessing different resources. However, it means that a multi-resource transaction must handle all the involved synchronization primitives correctly, which is difficult to do correctly without introducing deadlocks.

Having a shared synchronization for all resources in the wallet makes multi-resource transactions straightforward: once the synchronization primitive is acquired, all of the resources can be changed together. However, it reduces opportunities for parallelism and does not scale very efficiently to large numbers of threads and resources.

You are welcome to use either approach in this MP, but synchronizing each wallet, not each resource, is much easier to do.

3 MP Requirements (Part 4) - Wallet Server

You will implement a server for your wallet in the create_wallet_server function of wallet-server.py, which has the following requirements:

  1. Accept any number of concurrent connections.

    We have tests that sends as many as 100 connections at one time, so you’ll want to use a number at least that large as the argument of your server socket’s .listen method call or you might lose some of the connections.

  2. Listen incoming connections on the provided port, with each incoming connection must be then handled by its own thread.

  3. Accept messages over connected sockets and send send a message back for each one. All messages (incoming and outgoing) are terminated with a newline (\n). The messages are:

    • GET resource\n invokes the .get(resource) method of the underlying wallet and sends back its return value, converted to a str.

    • MOD resource delta\n invokes the .change(resource, delta) method of the underlying wallet and sends back its return value, converted to a str. You may assume that delta is an integer.

    • TRY resource delta\n invokes the .try_change(resource, delta) method of the underlying wallet and sends back its return value, converted to a str. You may assume that delta is an integer.

    • TRAN resource1 delta1 resource2 delta2 [...]\n for any number of resources (0 or more) invokes the .transaction(resource1=delta1, resource2=delta2, ...) method and sends back its return value, converted to a str. You may assume that each delta is an integer.

      Note that if d = {'arg1':2, 'another':3} then foo(**d) is equivalent to foo(arg1=2, another=3)

    • EXIT\n closes the socket connection, sending nothing and terminating the thread that was handling this connection.

  4. The only libraries you may import are threading and socket, as well as the wallet you created in parts 1–3. The command-line argument parser also imports getopt and sys, but you should not use those libraries in the code that you write. You should also not add any global variables that are not already part of the file.

Your server should ignore all \r characters. Just skip them. (This will help you debug, but is not a hard requirement and is not graded. However, telnet will add \r characters on some systems as part of the newline sequence, and the server will be much easier to test with telnet than without it.)

4 Testing your code

We recommend testing your implementation in three stages.

4.1 Stage 1: custom tests

The best way to see if your code works and fix it when it does not is do your own tests.

4.1.1 Parts 1–3: test programs

For parts 1–3, writing and running a tester file is the best way to make small tests.

Just implemented change and want to see if it works? Write a program like

import wallet
w = wallet.Wallet()
print(w.change('thingy',4))
print(w.change('thingy',-2))

and check its output against what you think it should do. This will be much easier to debug than any of the tests we provide.

4.1.2 Part 4: telnet

For part 4, you can use telnet to test your code. This will involve having two (or more) terminals.

In one you’ll start the server as python3 wallet-server.py -p 34000

In the other you’ll connect to it as telnet localhost 34000. After a few lines of connection information you’ll see something like

Connected to localhost.
Escape character is '^]'.

In the escape character string, ^ means the control key, so if you see the above to exit telnet you’d type Ctrl+].

You can then type messages in the telnet session, such as GET baz and the response will appear afterwards. For example, an entire telnet session might be

Connected to localhost.
Escape character is '^]'.
GET baz
0
MOD baz 6
6
MOD baz 4
10
GET baz
10
EXIT
Connection closed by foreign host.

You should be able to open multiple terminals at once, connect them all to the same server using telnet, and have them all interact with the wallet at once, including one telnet making a blocking request and being awakened by another telnet supplying the needed resources.

4.2 Stage 2: prebuilt files

The MP comes with several files that interact with the wallet in longer runs, verifying that your code works even in long-running sessions. These are:

hedgehog-simple.py

uses change and get

Two threads; one generates 🐛, the other consumes 🐛 to generate 🦔.

hedgehog-rat.py

uses change, get, and try_change

Four threads; one generates 🐛, one generates 🌽, one other consumes 🐛 to generate 🦔, and one tries to consume 🐛, but falls back on 🌽 if there are no 🐛, to generate 🐀.

Because thread scheduling is unpredictable, the number of times that 🐀s eat all the 🐛s before a 🦔 can will vary by run. There’s a very small chance that the program will freeze because it ran out of 🐛 and is still trying to make more 🦔.

degree.py

uses change, get, and transaction

Many threads.

Some threads just produce things: one generates ☘️ (and an occasional 🍀); another 🧰 (and an occasional 💎); another 🍏; another 🧬.

Some threads consume some things to generate other things, such as making a 📗 out of 1×🍏, 1×🍀, 10×☘️, and 5×🧬.

Eventually, these result in a 🎓.

gacha.py

uses change, get, and transaction

Many threads; like degree.py, some produce things and others convert them into other things.

This file mimics 90 draws of Genshin Impact’s 3-, 4-, and 5-star system with a pity system that guarantees an item of a certain star-level.

ping-pong.py

uses change

Two resources and two threads. One thread consumes one resource and produces the other, the other thread is the other way around. The result is each thread does one action then waits for the other, bouncing resources back and forth. This has proven useful for debugging some edge cases.

ping-pong-transaction.py

uses change and transaction

Like ping-pong.py, but using transactions instead of pairs of changes. Randomly decides which thread has the needed resource to start the exchanges.

4.3 Stage 3: pytest

We provide a suite of automated tests designed to be run with the pytest library. You can install pytest as

python3 -m pip install pytest
Getting pip

Different systems come with different ways to get pip. The most common is as a module runnable using python3 -m pip, as shown above, but you might need to do one of the following:

  • First run sudo apt-get install python3-pip (the VM will need this)
  • First run python3 -m ensurepip
  • Use pip or pip3 or py -m pip instead of python3 -m pip

Once it is installed, you can run the tests with

python3 -m pytest

You can also run just one part’s tests with python3 -m pytest test_part_1.py.

You can also run just a single test with python3 -m pytest test_part_1.py -k initially_empty.

These tests use several odd constructs to ensure they terminate even if your code blocks and are not likely to be easy to use in debugging directly. Some of them also require that your wallet.py does not print anything. If you fail one or more of these tests, you are best-off copying the relevant parts of the test into your own test program rather than trying to debug the test itself.

5 Modifiable Files

In your solution, you must only modify the following files. Modifications of other files may break things:

6 Submission and Grading

6.1 Submission

Once you have locally passed all the tests, you will need to submit and grade your code. First commit using the standard git commands:

git add -A
git commit -m "MP submission"
git push

6.2 Grading

The initial grading is done via a manual GitHub Action. You MUST complete this step before the deadline to earn any points for the MP:

6.3 Points

No points will be awarded unless the coding rules (limiting what you import and what globals you use) are followed. There is a test of these rules in test_coding_rules.py, which does not itself contribute points to your grade but which github will require you to pass before running any other tests.

Each automated test has equal weight.