Eric DayThoughts, code, and other oddments. |
Dark | Light |
|
|
|
< Older Entries | Archive for the "Gearman" CategoryOSCON and OpenStackJuly 26th, 2010
The past two weeks have been both exciting and extremely busy, first traveling to Austin, TX for the first OpenStack Design Summit, and then back home to Portland, OR for The O’Reilly Open Source Conference (OSCON) and Community Leadership Summit. The events were great in different ways, and there was some overlap with OpenStack since we announced it on the first day of OSCON and created quite a bit of buzz around the conference. I want to comment on a few things that came up during these two weeks. New RoleI’m now focusing on OpenStack related projects at Rackspace. I’m no longer working on Drizzle, but I will still be involved in the MySQL and database ecosystems through future projects and conferences (see you at OpenSQL Camp). I will also still be working on a couple of Gearman related projects in my spare time. At OSCON I gave two presentations on Gearman and Drizzle, you can find the slides here. The Five Steps to OpenOne question that came up a few times over the past couple weeks is what the term “Open” means when a business or organization decides to adopt the open source philosophy. It turns out this means many different things to folks, and when an organization decides to go open, they need to make a decision on how open they are willing to be. Here are the various layers we’ve seen over the years:
There have been examples of success for organizations who have stopped at each of these steps. Given the proper environment, any can work. My preference is to work on projects that are fully open, where company and organizational boundaries do not exist between developers and users. I’m thrilled to say that we’ve gone all in with OpenStack. We’re hosted on Launchpad and have a governance structure that allows all parties within the community to have a say in the future of the project. Preventing Vendor Lock-inDuring the Cloud Summit at OSCON, there was a debate titled: “Are Open APIs Enough to Prevent Lock-in?”. Most folks came to the conclusion that the answer is “no,” and I agree. While I feel open APIs are necessary, they are by no means sufficient. Even if a project is open source and allows for open development, it probably will not prevent vendor lock-in. The key is to provide some incentive for vendors to adopt and invest resources within a project. Much like customers don’t want vendor lock-in when choosing a platform, vendors do not want project or feature lock-in when choosing the software to power their business. Each vendor who chooses to participate must have the ability to voice their opinion on the direction of APIs, features, and other project priorities. This is why it is critical that any open source project must take all the steps described above to give the project a chance of being adopted and becoming the de facto standard. There is of course no guarantee that adoption and prevention of vendor lock-in will happen, but I see them as necessary steps. This is another area where OpenStack has done the correct thing. We are planning on having another developer summit in November, and then once every six months after that time. All design discussions and decision making will happen in public forums such as the mailing list and IRC. We want all participants in the community to have a chance to respond to topics being discussed, and we believe the more we have, the more successful the project will be. Having many voices allows the project to be more applicable to different environments. For example, Rackspace and NASA have different requirements for their compute architectures, but they also share many components as well. Through open participation we can ensure all needs are accounted for. Much like the LAMP stack has powered universities, governments, and competing business, we hope OpenStack can do the same. Contributor License Agreement (CLA)During the past couple of weeks a few folks asked what the CLA was all about. When the foundations of OpenStack were forming, the requirement of having a CLA came up from the legal side. Having been involved with open source projects that had very invasive CLAs, initially I had quite a bit of concern. The CLA is actually quite innocuous, and it does NOT require assignment or dual-ownership of copyright. You are the sole owner of code you contribute. For all intents and purposes it is a signed version of the Apache 2.0 license, the CLA just makes these terms more explicit. The CLA is handled through digital signatures, so no papers, pens, or faxing is required. Get Involved!Expect to see more posts on my blog related to OpenStack topics. If you would like to get involved, you can join the IRC channel (#openstack on irc.freenode.net), join the mailing list, or start contributing code! There are even jobs around OpenStack popping up already! Posted in Drizzle, Gearman, Main, MySQL, OpenStack | 4 CommentsThreads with EventsApril 20th, 2010Last week I was surprised to see this paper bubble back up on Planet MySQL. It describes the pros and cons of thread and event based programming for high concurrency applications (like a web server), arguing that thread-based programming is superior if you use an appropriate lightweight threading implementation. I don’t entirely disagree with this, but the problem is such a library does not exist that is standard, portable, and useful for all types of applications. We have POSIX threads in the portable Linux/Unix/BSD world, so we need to work with this. Other experimental libraries based on lightweight threads or “fibers” are really interesting as they can maintain your stack without all the normal overhead, but it is hard to get the scheduling correct for all application types. I would even argue that thread and event based programming is actually not all that different, it’s just a matter of how state is maintained (stack vs state variables) and how scheduling is performed. The comparisons done in that paper also put a C-based web server using a co-routine threading library against a Java based server that depends on the poll() system call. I’m sorry, but this is comparing apples to oranges. First, you’re in the Java VM with a number of runtime components (like garbage collection) which may be getting in the way. Also, the standard poll() system call is not an efficient event-handling mechanism, it’s much better to use epoll or some other Kernel-based handling mechanism. One high-concurrency userland threading implementation I do like is in Erlang. Erlang processes are extremely lightweight and I’ve written apps that depend heavily on them. One interesting application I saw was caching objects where each object got it’s own Erlang process. This put a whole new spin on cache management, and it looked like it could actually scale reasonably well. The “problem” with Erlang, which may or may not be a problem depending on your requirements, is that it is still a bit of overhead running byte-code in a VM, as well as it being a functional language. I love functional programming, but I’ve found it still ties most developer’s heads in knots if they don’t have a reason to use it regularly. For open source projects trying to build a contributor community, it can act as one more hurdle. So, what is the “best” paradigm? Back in 2000 some colleagues and I wrote a hybrid thread-event library that would create one event-handler instance per thread, and connections would be spread across the pool of event-handling threads. I believe this gave the best of both worlds, and I saw high throughputs with fairly minimal overhead. I wrote a number of servers based on this architecture, including HTTP, IMAP, POP3, and DNS, and with each server type this model proved to be efficient and scalable. Ultimately the best architecture depends on your application. If you never intend to have many connections, and your applications has long-running computations, one-thread-per-connection would probably be best. If you need to handle large numbers of connections and have short, non-blocking request processing, event-based scales extremely well. You can of course create a hybrid of these two and have all connections managed by event threads and asynchronous queues to dedicated processing threads for heavy request handling (this is sort of what I did in the C Gearman Job Server). There is no single correct answer, so take a look at your options before deciding how to approach your own applications. Don’t be afraid to create hybrids as well. Regardless of which paradigm you choose, concurrent programming can be hard, especially at the lower levels. There have been a number of higher level abstractions to help developers, from new libraries to new languages, but most of these come with a cost in performance or flexibility. When you need to squeeze every bit of performance out of your application, you will most likely end up in C or C++ dealing with these issues directly. This is actually one of the problems I’m attempting to address with the Scale Stack Event modules. I’m trying to create a healthy level of abstraction on hybrid thread/event based applications so you don’t have any overhead or limitations while a lot of the common headaches are taken care of for you. If you have a need for such a system, get in touch, I’d be interested to talk. Since it is BSD licensed you can use it in any application, including commercial. Posted in Drizzle, Gearman, Main, MySQL | 5 CommentsGearman Releases and Talks at the MySQL ConferenceApril 5th, 2010I spent some time this weekend fixing up the Gearman MySQL UDFs (user defined functions) and fixed a few bugs in the Gearman Server. You can find links to the new releases on the Gearman website. The UDFs now use Monty Taylor’s pandora-build autoconf files instead of the old fragile autoconf setup that relied on pkgconfig. If you are attending the MySQL Conference & Expo next week and want to learn more about Gearman, be sure to check out one of the three sessions Giuseppe Maxia and I are giving:
Hope to see you there! Posted in Drizzle, Gearman, Main, MySQL | No CommentsDrizzling from the Rackspace CloudMarch 8th, 2010Since I left Sun back in January, folks have been asking what was next. I’m happy to say that I’m going to continue hacking on open source projects like Drizzle and Gearman, but now at the Rackspace Cloud. Not only will I be there, but I get to continue working closely with a few of the amazing Drizzle hackers who have also joined, including Monty Taylor, Jay Pipes, Stewart Smith, and Lee Bieber. Why Rackspace Cloud? Late last year I was considering what I wanted to do next with the Oracle acquisition looming near, and this was one of the options that presented itself. Rackspace had been a supporter of Drizzle from early on by offering virtual machines to develop and test on, and when talking to some folks more closely, something really hit home. Rackspace provides first-class service and “fanatical” support – they are not a software company. One might ask why an open source software developer would be interested in a company that doesn’t create software or vice-versa, and the answer is that Rackspace wants to find ways to offer the best possible service now and into the future. What better way than to help develop the next generation of service software and get a jump start into integrating this into their architecture? Both the open source community and Rackspace win. Another thing I learned while talking with Rackspace is that one of their core principles is transparency. This applies to both customer and employees, and anyone within an open source community can appreciate this. The more I learned about the company and the folks within it, the more impressed I was at the lack of internal barriers or “need-to-know” information. One of Drizzle’s core goals is also transparency, from discussing design decisions on public mailing lists and IRC, to having the entire project management infrastructure hosted out in the open at Launchpad. What does this mean for the Drizzle project? It means continued support for a number of core developers, more infrastructure for development, and most importantly in my eyes, more context. One of the Drizzle tag-lines is “A Lightweight SQL Database for Cloud and Web,” so what better place to develop a database designed for the cloud than on one of the fastest growing cloud platforms. We’ll get a detailed look at the demands, get feedback from cloud customers, and have the perfect test bed for offering new services. We’ll also be able to work closely with a top-notch group of DBAs, developers, and sysadmins in one of the most demanding service architectures out there. This invaluable context will help the Drizzle developers make more informed decisions moving forward, which also means better software for the community. Personally, this also means getting back to my hosting roots. Before Sun, I worked at Concentric for almost 10 years in a clustered hosting environment. I’m very familiar with many of the multi-tenant scalability concerns Rackspace has, and I’m excited to be working in this type of environment again. We’ve already been working closely with the MySQL DBAs at Rackspace to learn what the biggest pain points are for a multi-tenant architecture, and we’ll be taking steps to address these as it will help anyone wanting to run Drizzle in a cloud-like environment. Drizzle’s modular architecture has already proved useful, as some of these concerns are easily answered with “oh, we have a plugin point for that.” I’m excited, this is going to be a fun ride. Posted in Drizzle, Gearman, Main, MySQL | 16 CommentsMySQL Conf & Drizzle Dev DayFebruary 17th, 2010I’m glad to announce that we’ll be having a Drizzle developer day again this year on the Friday after the MySQL Conference! Be sure to sign up and add any topic ideas you may have so we know what folks are interested in. Space is limited! While at the MySQL Conference, I’ll be speaking with Monty Taylor on “Using Drizzle.” This will take a non-developer approach to the project, so everyday DBAs and web developers should find this interesting. I’ll also be teaming up with Giuseppe Maxia to talk about Gearman in three sessions. These include:
We’re also going to have a combo Drizzle/Gearman booth in the expo hall, so be sure to stop by and chat. See you there! Posted in Drizzle, Gearman, MySQL | No CommentsLinux Conf AU 2010February 3rd, 2010I was really excited when I had my Gearman talk accepted to Linux Conf AU 2010 because I had never been out that far in the Pacific (only Hawaii). Of course it wasn’t in Australia this year, and instead in Wellington, New Zealand. My wife came too, and we also made a vacation out of the down times we had around the conference. It turned out Brian couldn’t make it this year so Monty, Stewart, and I gave the Drizzle talk. It was great to see some familiar faces, including Mark Atwood, Giuseppe Maxia, Josh Berkus, and Selena Deckelmann. Josh actually ended up being on the same flight out, so we got to catch up while going through New Zealand customs at 5am after a 13 hour flight. :) New Zealand is an amazing place. We flew in and out of Auckland and took the train to Wellington. The train ride mostly consisted of grazing sheep once out of the metro areas, did you know there are more sheep than people in NZ? Beyond the sheep, there were great views along the way, especially in the middle near the larger mountains and volcanoes. We stopped for a day to hike the Tongariro Alpine Crossing. It was sunny when we started, but it it was raining with 40mph winds at the top, so we didn’t get to see as much as we hoped. There were still beautiful views on each side though. The conference was very well run, thanks to anyone who had a hand in it! The speakers dinner was at this great museum nearby on the waterfront and included live Maori singing and dancing. The vegan options were tasty, and I got to meet a few interesting folks there (like the folks from Dreamwidth, a LiveJournal-like blogging service). Some notable sessions during the conference were “The World’s Worst Inventions” by Paul Fenwick, “Anti-features” Keynote by Benjamin Mako Hill, “The Hydras GCC Static Analysis Plugins” by Taras Glek, and “Simplicity Through Optimization” by Paul McKenney. There were many other great sessions, and some I wish I could have attended. I’m certainly going to try to go again next year, which if you didn’t hear will be in Brisbane! Posted in Drizzle, Gearman, Main | No CommentsMoving OnJanuary 11th, 2010Friday was my last day at Sun Microsystems, and today is the first day at my new job (location coming soon). I’ve had a great time at Sun, and thank them for all the opportunities given to me there. I’ll be doing mostly the same work at the new gig, working on projects like Drizzle, but with a slightly different focus. For the most part my day-to-day won’t change much. Right now I’m focusing on libdrizzle again and am implementing the prepared statement API, cleaning up the MySQL protocol support a little, and also implementing the new Drizzle client/server protocol. I’ll continue to work on Gearman as well, especially where it is relevant to Drizzle. I also need to start blogging again with specific topics in the projects I’m working on, I’ve been fairly quiet lately. I’ll be in New Zealand next week at Linux Conf AU (yes, it’s not in AU this year). I have a talk on Gearman, and it looks like I’ll also be helping out with the Drizzle talk. It will be really nice to escape the Portland, OR winter for a bit. :) Posted in Drizzle, Gearman, Main, MySQL | 4 CommentsNon-blocking State MachinesOctober 7th, 2009If you’ve ever done any non-blocking programming (usually for socket I/O), you’ve probably had to come up with a non-trivial state machine to handle all the places where everything can pause. Say you’re reading an application level packet from a socket, and half way through the read() system call it screams EAGAIN. You need to stop, save any state, and exit out of whatever chain of functions got you there so the calling application can regain control. I’m going to explain a few techniques I’ve come up with over the years, each with their strengths and weaknesses, and I hope this will spur some conversation of what other folks have done. While I’m fairly happy with how I handle these state machines now, but I’m always looking for a more succinct way of handling things. Please share your thoughts! Switch Statements The obvious way to handle non-blocking I/O is with one or more switch statements. Say we need to check the status of something by sending a request over a TCP connection, possibly connecting to the remote host first, and then reading the response. Here is a bit of pseudo-code that demonstrates how this could work (ignoring some error cases, efficient buffer handling, and non-blocking connect cases):
int check_status(struct connection *con)
{
switch (con->state)
{
case CONNECTION_STATE_NONE:
getaddrinfo(...);
con->fd = socket(...);
/* Fall through to next state. */
case CONNECTION_STATE_CONNECT:
ret = connect(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_CONNECT;
return WAIT_FOR_WRITE;
}
/* Fall through to next state. */
case CONNECTION_STATE_REQUEST:
ret = write(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_REQUEST;
return WAIT_FOR_WRITE;
}
/* Fall through to next state. */
case CONNECTION_STATE_RESPONSE_HEADER:
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_RESPONSE_HEADER;
return WAIT_FOR_READ;
}
/* Save header. */
/* Fall through to next state. */
case CONNECTION_STATE_RESPONSE:
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_RESPONSE;
return WAIT_FOR_READ;
}
/* Save response. */
/* Set this here so we skip the connect state next time around. */
con->state = CONNECTION_STATE_REQUEST;
break;
}
}
The first thing you may cringe at is the fall-through cases in switch statements. The alternative is to set a new state at the end of each case, break, and then reevaluate the switch again with that new state (wrapping the above switch in a while loop). I skipped that version since those are some extra ops that are just not necessary. The above machine may be a bit clunky, but it works for simple cases. But what about when you have more complex states that have loops, non-sequential state execution, or nested switch statements? The above has the potential to grow into an unwieldy mess of code. For example, say if we need to read multiple responses back in the last state above, this could be expanded to:
int check_status(struct connection *con)
{
switch (con->state)
{
...
/* Fall through to next state. */
case CONNECTION_STATE_RESPONSE:
while (1)
{
if (con->need_header)
{
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_RESPONSE;
return WAIT_FOR_READ;
}
/* Save header. */
con->need_header = false;
}
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_RESPONSE;
return WAIT_FOR_READ;
}
/* Save response. */
if (last_response)
break;
con->need_header = true;
}
/* Set this here so we skip the connect state next time around. */
con->state = CONNECTION_STATE_REQUEST;
break;
}
}
As you can see, another state variable has been added as a boolean (con->need_header). What if responses were not made up of simple header and body? What if there are more nested levels? We can add more switch statements and start breaking this up some into nested functions to make it more readable, but the complexity is still there. For non-trivial non-blocking state machines, this approach is not scalable. Nested switch/while Statements Early on in my C years I stumbled upon Duff’s Device. At first I was confused, is that even valid C? Oh, it compiles! Then I was offended. Eventually it clicked and I appreciated the cleverness of the code. Nesting while/for/if with switch statements. I went off to re-write my non-blocking state machines with this new trick:
int check_status(struct connection *con)
{
switch (con->state)
{
...
/* Fall through to next state. */
while (1)
{
case CONNECTION_STATE_RESPONSE_HEADER:
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_RESPONSE_HEADER;
return WAIT_FOR_READ;
}
/* Save header. */
/* Fall through to next state. */
case CONNECTION_STATE_RESPONSE:
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
{
con->state = CONNECTION_STATE_RESPONSE;
return WAIT_FOR_READ;
}
/* Save response. */
if (last_response)
break;
}
/* Set this here so we skip the connect state next time around. */
con->state = CONNECTION_STATE_REQUEST;
break;
}
}
Yup, that’s correct. Shove that while loop right in there. Think of it this way: write your state machine as you would if it were blocking, nesting as deep as you need with for/if/while statements. Next, put a switch around the entire thing, and toss a case statement in wherever something could hit a non-blocking condition (regardless of scope or nesting level). Some folks have commented this feels a lot like using gotos, but I disagree. With switch, you have structure, and compiler warnings for when things are missing (like a case). Sure, it may not be the most elegant solution, but you avoid the nested switch statements and multiple state variables. I still use this today for some things (like inside of the Gearman C server and library), but only when they are fairly simple state machines. Function Pointer Stack Last year I started writing a non-blocking C library for MySQL. When I head about Drizzle, I decided to focus my effort there (while keeping the MySQL compatibility), and renamed it to libdrizzle. Today it supports the Drizzle protocol and the most common parts of the MySQL protocol. The protocols for these projects are a bit more involved, so when I began writing the library, I went through a few iterations of state machines and didn’t find anything I was happy with. After some brainstorming I came up with an alternative design, I usually refer to it as a “function pointer stack” or “callback stack”. Please let me know if you have seen something like this and point me to the proper name. :) This works by creating a traditional stack (LIFO structure) of function pointers. When a state needs to be executed, push it on, when a state is complete, it can pop itself off. It’s similar to a program execution stack, but maintained in user space and state is kept so you know where things left off. Still not getting it? Lets look at the code. First, one quick note about function pointer typedefs: typedef int (state_fn)(struct connection *con); These are not required of course, but it makes things a bit more legible. This is saying ‘state_fn’ is now a type that points to a function with the given signature. It’s a lot easier that having to write the function signature out every time you have a variable of this type. Now, the code:
typedef int (state_fn)(struct connection *con);
struct connection
{
...
state_fn *state_stack[STACK_SIZE];
int state_current;
};
/* These functions operation on the function pointer stack. */
static inline bool state_none(struct connection *con)
{
return con->state_current == 0;
}
static inline void state_push(struct connection *con, state_fn *function)
{
assert(con->state_current < STACK_SIZE);
con->state_stack[con->state_current]= function;
con->state_current++;
}
static inline void state_pop(struct connection *con)
{
con->state_current--;
}
int state_run(struct connection *con)
{
int ret;
while (!state_none(con))
{
ret= con->state_stack[con->state_current - 1](con);
if (ret)
return ret;
}
return 0;
}
/* These are the states that can be pushed onto the stack. */
int start_state(struct connection *con)
{
getaddrinfo(...);
con->fd = socket(...);
state_pop(con);
state_push(con, connect_state);
return 0;
}
int connect_state(struct connection *con)
{
ret = connect(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
return WAIT_FOR_WRITE;
state_pop(con);
return 0;
}
int request_state(struct connection *con)
{
if (not connected)
{
state_push(con, start_state);
return 0;
}
ret = write(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
return WAIT_FOR_WRITE;
state_pop(con);
state_push(con, response_header_state);
return 0;
}
int response_header_state(struct connection *con)
{
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
return WAIT_FOR_READ;
/* Save header. */
state_pop(con);
state_push(con, response_state);
return 0;
}
int response_state(struct connection *con)
{
ret = read(con->fd, ...);
if (ret == -1 && errno == EAGAIN)
return WAIT_FOR_READ;
/* Save response. */
state_pop(con);
if (have_more_responses)
state_push(con, response_header_state);
return 0;
}
/* Here is a function you would make public in the API to start the state machine. */
int check_status(struct connection *con)
{
/* If we are coming back into this after a blocking event,
make sure we don't push a new state on again. */
if (state_none(con))
state_push(con, request_state);
return state_run(con);
}
As you can see, we still start in the check_status function, but push a state if there is no state and then go into our run loop. You can follow along the various functions (sort of like a choose your own adventure book) but eventually you should end up with an empty stack. When this happens, the state_run() function returns 0 and the call is complete. This may be a bit overkill for such a simple state machine, but as your state execution flow becomes non-sequential (random jumps, recursion, …) the power and flexibility of this design becomes apparent. And what? No switch statements? As far as performance is concerned, you may have more function calls, but you are eliminating jumps (those nested if/switches). For example, if your state is five levels deep and you need to keep pausing and returning to that point, you hit all those switch statements every time. With the above approach? You jump directly into the function you left off in. I’m not sure which one is faster in general (really depends on application), but the cost of switches vs function calls will be insignificant compared to what normal applications are actually doing (like system calls for I/O). I have working C and C++ examples of what a complete state machine looks like. There is also some micro-benchmarking numbers in there comparing C vs C++ (you take a hit in C++ due to inheritance, but that cost is fairly insignificant). Thoughts? Gearman News and ReleasesOctober 7th, 2009The past week has brought a surge of Gearman related releases. They include: C Server and Library Some of these releases were driven by the C API changes I made to clean a few things up, but a fair amount of functionality was added to the C library and C based modules (like timeouts and non-blocking API clients and workers). The Perl server included a number of algorithm improvements in worker selection. I’ll be taking a closer look at those and including them in the C server for the next release. Rasmus Lerdorf took Gearman for a spin in PHP, and a Gearman implementation in Erlang was even announced this week. Thanks to everyone in the Gearman community for all your hard work! Posted in Drizzle, Gearman, Main, MySQL | No CommentsGearman Slides from San Francisco MeetupOctober 5th, 2009Thanks to everyone who came out to the San Francisco PHP and MySQL meetup! Also, thanks to Michael for organizing such a great event, and Percona for sponsoring the food. I put the slides from the talk up on my wiki for reference or in case you missed it. I believe that there will be a video up at some point as well. While down there I also had a chance to stop by Digg and talked to them about Gearman (they’ve been using it for a while). It was interesting to see how they were using it in a large scale deployment. I was able to get some valuable feedback to future development, and a cool t-shirt. :) Thanks Digg! Posted in Gearman, Main, MySQL | No Comments< Older Entries | |
Blog Wiki About Resume RSS Comments Launchpad identi.ca OpenStack Scale Stack Gearman NW Veg Veg Food & Fit |
|
Copyright (C) Eric Day - eday@oddments.org All content licensed under the Creative Commons Attribution 3.0 License. Hosted by Rackspace Cloud |
|