LinqToTwitter User Stream memory leak - linq-to-twitter

I'm having an issue with LinqToTwitter 4.1 where having a user stream open will eventually cause the programs memory usage to balloon out of control. This does not occur when the program starts running but only after some time, normally after a day or two.
Using the ANTS Memory Profiler I find this reference chain preventing System.Byte[] from being collected. The full profiler results can be downloaded here.
Code:
private async Task<Streaming> TwitterSteam(string trackHashTags, string twitterUserIds)
{
var stream = (from strm in TwitterCtx.Streaming.WithCancellation(CloseStreamSource.Token)
where strm.Type == StreamingType.Filter &&
strm.Track == trackHashTags
&& strm.Follow == (string.IsNullOrEmpty(twitterUserIds) ? "41553192" : twitterUserIds)
select strm).StartAsync(async strm =>
{
string message = string.IsNullOrEmpty(strm.Content) ? "Keep-Alive" : strm.Content;
if (message == "Keep-Alive")
{
IsRunning = true;
}
else
{
JsonData data = JsonMapper.ToObject(message);
Status tweet = new Status(data);
LogClient.LogInfo("Received Tweet: " + tweet.Text, null, LogType.Info, null);
ConvertToMessage(tweet);
IsRunning = true;
}
}).Result.SingleOrDefault();
return stream;
}
Can anyone provide insight as to why this is occurring and how I can prevent it?

Related

How do you code a purge command

So I have been search far and wide over the internet, trying to find a possible way to make a purge command. Now I have found quite a lot of different ways to make one, but none of them either suited me in the way I wanted, or simply worked for me.
SO to start off, here is my code
const Discord = require("discord.js"); // use discord.js
const BOT_TOKEN = "secret bot token :)" // bot's token
const PREFIX = "*" // bot's prefix
var eightball = [ // sets the answers to an eightball
"yes!",
"no...",
"maybe?",
"probably",
"I don't think so.",
"never!",
"you can try...",
"up to you!",
]
var bot = new Discord.Client(); // sets Discord.Client to bot
bot.on("ready", function() { // when the bot starts up, set its game to Use *help and tell the console "Booted up!"
bot.user.setGame("Use *info") // sets the game the bot is playing
console.log("Booted up!") // messages the console Booted up!
});
bot.on("message", function(message) { // when a message is sent
if (message.author.equals(bot.user)) return; // if the message is sent by a bot, ignore
if (!message.content.startsWith(PREFIX)) return; // if the message doesn't contain PREFIX (*), then ignore
var args = message.content.substring(PREFIX.length).split(" "); // removes the prefix from the message
var command = args[0].toLowerCase(); // sets the command to lowercase (making it incase sensitive)
var mutedrole = message.guild.roles.find("name", "muted");
if (command == "help") { // creates a command *help
var embedhelpmember = new Discord.RichEmbed() // sets a embed box to the variable embedhelpmember
.setTitle("**List of Commands**\n") // sets the title to List of Commands
.addField(" - help", "Displays this message (Correct usage: *help)") // sets the first field to explain the command *help
.addField(" - info", "Tells info about myself :grin:") // sets the field information about the command *info
.addField(" - ping", "Tests your ping (Correct usage: *ping)") // sets the second field to explain the command *ping
.addField(" - cookie", "Sends a cookie to the desired player! :cookie: (Correct usage: *cookie #username)") // sets the third field to explain the command *cookie
.addField(" - 8ball", "Answers to all of your questions! (Correct usage: *8ball [question])") // sets the field to the 8ball command
.setColor(0xFFA500) // sets the color of the embed box to orange
.setFooter("You need help, do you?") // sets the footer to "You need help, do you?"
var embedhelpadmin = new Discord.RichEmbed() // sets a embed box to the var embedhelpadmin
.setTitle("**List of Admin Commands**\n") // sets the title
.addField(" - say", "Makes the bot say whatever you want (Correct usage: *say [message])")
.addField(" - mute", "Mutes a desired member with a reason (Coorect usage: *mute #username [reason])") // sets a field
.addField(" - unmute", "Unmutes a muted player (Correct usage: *unmute #username)")
.addField(" - kick", "Kicks a desired member with a reason (Correct usage: *kick #username [reason])") //sets a field
.setColor(0xFF0000) // sets a color
.setFooter("Ooo, an admin!") // sets the footer
message.channel.send(embedhelpmember); // sends the embed box "embedhelpmember" to the chatif
if(message.member.roles.some(r=>["bot-admin"].includes(r.name)) ) return message.channel.send(embedhelpadmin); // if member is a botadmin, display this too
}
if (command == "info") { // creates the command *info
message.channel.send("Hey! My name is cookie-bot and I'm here to assist you! You can do *help to see all of my commands! If you have any problems with the Minecraft/Discord server, you can contact an administrator! :smile:") // gives u info
}
if (command == "ping") { // creates a command *ping
message.channel.send("Pong!"); // answers with "Pong!"
}
if (command == "cookie") { // creates the command cookie
if (args[1]) message.channel.send(message.author.toString() + " has given " + args[1].toString() + " a cookie! :cookie:") // sends the message saying someone has given someone else a cookie if someone mentions someone else
else message.channel.send("Who do you want to send a cookie to? :cookie: (Correct usage: *cookie #username)") // sends the error message if no-one is mentioned
}
if (command == "8ball") { // creates the command 8ball
if (args[1] != null) message.reply(eightball[Math.floor(Math.random() * eightball.length).toString(16)]); // if args[1], post random answer
else message.channel.send("Ummmm, what is your question? :rolling_eyes: (Correct usage: *8ball [question])"); // if not, error
}
if (command == "say") { // creates command say
if (!message.member.roles.some(r=>["bot-admin"].includes(r.name)) ) return message.reply("Sorry, you do not have the permission to do this!");
var sayMessage = message.content.substring(4)
message.delete().catch(O_o=>{});
message.channel.send(sayMessage);
}
if(command === "purge") {
let messagecount = parseInt(args[1]) || 1;
var deletedMessages = -1;
message.channel.fetchMessages({limit: Math.min(messagecount + 1, 100)}).then(messages => {
messages.forEach(m => {
if (m.author.id == bot.user.id) {
m.delete().catch(console.error);
deletedMessages++;
}
});
}).then(() => {
if (deletedMessages === -1) deletedMessages = 0;
message.channel.send(`:white_check_mark: Purged \`${deletedMessages}\` messages.`)
.then(m => m.delete(2000));
}).catch(console.error);
}
if (command == "mute") { // creates the command mute
if (!message.member.roles.some(r=>["bot-admin"].includes(r.name)) ) return message.reply("Sorry, you do not have the permission to do this!"); // if author has no perms
var mutedmember = message.mentions.members.first(); // sets the mentioned user to the var kickedmember
if (!mutedmember) return message.reply("Please mention a valid member of this server!") // if there is no kickedmmeber var
if (mutedmember.hasPermission("ADMINISTRATOR")) return message.reply("I cannot mute this member!") // if memebr is an admin
var mutereasondelete = 10 + mutedmember.user.id.length //sets the length of the kickreasondelete
var mutereason = message.content.substring(mutereasondelete).split(" "); // deletes the first letters until it reaches the reason
var mutereason = mutereason.join(" "); // joins the list kickreason into one line
if (!mutereason) return message.reply("Please indicate a reason for the mute!") // if no reason
mutedmember.addRole(mutedrole) //if reason, kick
.catch(error => message.reply(`Sorry ${message.author} I couldn't mute because of : ${error}`)); //if error, display error
message.reply(`${mutedmember.user} has been muted by ${message.author} because: ${mutereason}`); // sends a message saying he was kicked
}
if (command == "unmute") { // creates the command unmute
if (!message.member.roles.some(r=>["bot-admin"].includes(r.name)) ) return message.reply("Sorry, you do not have the permission to do this!"); // if author has no perms
var unmutedmember = message.mentions.members.first(); // sets the mentioned user to the var kickedmember
if (!unmutedmember) return message.reply("Please mention a valid member of this server!") // if there is no kickedmmeber var
unmutedmember.removeRole(mutedrole) //if reason, kick
.catch(error => message.reply(`Sorry ${message.author} I couldn't mute because of : ${error}`)); //if error, display error
message.reply(`${unmutedmember.user} has been unmuted by ${message.author}!`); // sends a message saying he was kicked
}
if (command == "kick") { // creates the command kick
if (!message.member.roles.some(r=>["bot-admin"].includes(r.name)) ) return message.reply("Sorry, you do not have the permission to do this!"); // if author has no perms
var kickedmember = message.mentions.members.first(); // sets the mentioned user to the var kickedmember
if (!kickedmember) return message.reply("Please mention a valid member of this server!") // if there is no kickedmmeber var
if (!kickedmember.kickable) return message.reply("I cannot kick this member!") // if the member is unkickable
var kickreasondelete = 10 + kickedmember.user.id.length //sets the length of the kickreasondelete
var kickreason = message.content.substring(kickreasondelete).split(" "); // deletes the first letters until it reaches the reason
var kickreason = kickreason.join(" "); // joins the list kickreason into one line
if (!kickreason) return message.reply("Please indicate a reason for the kick!") // if no reason
kickedmember.kick(kickreason) //if reason, kick
.catch(error => message.reply(`Sorry #${message.author} I couldn't kick because of : ${error}`)); //if error, display error
message.reply(`${kickedmember.user.username} has been kicked by ${message.author.username} because: ${kickreason}`); // sends a message saying he was kicked
}
});
bot.login(BOT_TOKEN); // connects to the bot
It is the only file in my bot folder except the package.json, package-lock.json and all the node_modules.
What I'm trying to do is type in discord *purge [number of messages I want to purge] and get the bot to delete the amount of the messages I asked it to delete, PLUS the command I entered (for example, if I ask the bot to delete 5 messages, he deletes 6 including the *purge 5 message.
Any help would be much appreciated, thanks!
What you are looking for is this (bulkDelete()) method for Discord.js.
It bulk deletes a message, just simply pass in a collection of message in the method and it will do the job for you.
(You can use messages property from channel, otherwise if you prefer promises, then try fetchMessages() method. )
Just make sure that the channel is not a voice channel, or a DM channel. And finally, your bot needs to have permission too.
You can get your own bot's permission for the guild using message.guild.member(client).permissions, or you can just directly use message.guild.member(client).hasPermission(permission), which returns a boolean that determines if your bot have a desired permission.
(The method docs for hasPermission() is here)
note sure if this is still relevant but this what i use in my bots
if (!suffix) {
var newamount = "2";
} else {
var amount = Number(suffix);
var adding = 1;
var newamount = amount + adding;
}
let messagecount = newamount.toString();
msg.channel
.fetchMessages({
limit: messagecount
})
.then(messages => {
msg.channel.bulkDelete(messages);
// Logging the number of messages deleted on both the channel and console.
msg.channel
.send(
"Deletion of messages successful. \n Total messages deleted including command: " +
newamount
)
.then(message => message.delete(5000));
console.log(
"Deletion of messages successful. \n Total messages deleted including command: " +
newamount
);
})
.catch(err => {
console.log("Error while doing Bulk Delete");
console.log(err);
});
basic function is to specify number of messages to purge and it will delete that many plus the command used, the full example is here

discord.js: JSON planet property is undefined

I'm making a Discord bot using discord.js, and I'm starting to add JSON stuff to it, so that I can store info for individual users, in a separate file. However, I keep getting an error that says planet is not defined, at the line that says if (bot.log[mentionedGuyName].planet == undefined) {. There are some variables, modules etc. in here that haven't been declared or whatnot, but that's only because if I put all my code on here, it would be pages long. My JSON file is called log.json.
The general purpose of this code block, if it helps, is to see if the user already has a "planet". If so, the bot finds gets that value from the JSON file, and sends it to the channel. If not, then it picks a random one (code I didn't put here because of size)
I think I understand at least kind of why the error is occurring (the planet property isn't defined), but I'm not sure how to fix it. If anyone knows how to declare a JSON property or whatever is going on here, I and my server would be most grateful. Thanks in advance!
Here's my JavaScript file:
let mentionedGuy = message.mentions.members.first();
let mentionedGuyName = null;
let noMentions = message.mentions.members.first() == false ||
message.mentions.members.first() == undefined;
if (noMentions) return;
else mentionedGuyName = mentionedGuy.user.username;
if (message.content.startsWith(prefix + "planet")) {
if (message.content.length > 7) {
if (bot.log[mentionedGuyName].planet == undefined) {
bot.log[mentionedGuyName] = {
planet: jMoon
}
fs.writeFile('./log.json', JSON.stringify(bot.log, null, 4), err => {
if (err) throw err;
});
message.channel.send(targeting);
message.channel.send(coords);
} else {
message.channel.send(bot.log[mentionedGuyName].planet);
}
}
}
Change it so it checks the typeof
if(typeof <var> == 'undefined') //returns true if undefined
Noting that typeof returns a string
Source

discord.js - I need help for a profile command

So, I've created a profile command for my bot but I wanna add an second argument.
The command that I wanna make is "/profile [username to search]" , but I don't now how to do it.
If you can help me, I'll appreciate this.
Here is the code:
if (message.content === '/profile') {
let botembed = new Discord.RichEmbed()
.setTitle("**__Exoly User Profile__**")
.setTimestamp(new Date())
.setColor("#4286f4")
.setFooter("Exolia", `${bot.user.avatarURL}`)
.setThumbnail(`${message.author.avatarURL}`)
.addField("Username :", `${message.author.username}`, inline = true)
.addField("Exolytes :", "|---|", inline = true)
.addField("Played Time :", "|---|", inline = true)
.addField("Faction :", "Armada", inline = true);
if (shouldResponseTo(message)) {
message.delete()
return message.channel.send(botembed);
}
}
A quick nice way to do what you want,
//Checks if it starts with (/profile)
if(message.content.startsWith(`/profile`)){
//Defines the user that needs to be searched to be either the user pinged OR the user that called the command
let user = message.mentions.users.first(); || message.author;
let embed = new Discord.RichEmbed()
.setTitle(`Profile`);
.setDescription(`Profile of ${user.username}`);
//Note: .send(embed) is allowed but not recommended and it's easier and safer todo .send({embed}) or in your case .send({embed:botembed})
message.channel.send({embed});
}
You could also check what was said AFTER (/profile) by doing:
let args = message.content.split(` `);
if(args[0]===`/profile`){
args.shift();
//args is now all the arguments that was put in after (/profile)
//So a message (/profile #user page 3)
//args would equal ["#user", "page", "3"]
}

Some questions regarding my first contract using local RPC , Web3 and Remix

I'm newbie with solidity and I created my first smart contract for a POC.
The idea is to simulate a reservation process where the guest pays an initial deposit (unlockDoor method) and, when he leaves the room, he will get money back based on the time of usage.
I connected events to my raspberry in order to turn on the lights of the related rooms.
It works with a javascript virtual machine but with a local RPC I have some issues and I do not understand why.
Using simple buttons inside an html page, unlockDoor and lockDoor methods do not open the metamask popup for accepting the transaction. no errors inside the console.
Using remix with local RPC: unlock door works, lock door generates error Error: VM Exception while executing transaction: out of gas.
A lot of articles say to increase gas value but it does not work. Probably I missed something. I do not understand what.
Using javascript virtual machine all methods work properly.
Probably the double transfer inside the lock method generates something strange using RPC (and test net). Are these double operations correct? Do I have to manage them in another way?
based on point 2 and 3: have generated confusion on how to use the "payable" instruction.
the javascript of Index.html
var web3 = new Web3(new
Web3.providers.HttpProvider("http://localhost:8545"));
web3.eth.defaultAccount = web3.eth.accounts[0];
var hotelReservation = web3.eth.contract(ABI);
var contract = hotelReservation.at(ADDRESS);
var room1_unlock = document.getElementById("room1");
room1_unlock.addEventListener("click", function(){
console.log("here");
contract.unlockDoor(1);
});
var room1_lock = document.getElementById("room1_lock");
room1_lock.addEventListener("click", function(){
console.log("here");
contract.lockDoor(1);
});
The contract.
Note: cost is per second for testing pourpose only
contract HotelReservation{
//the owner of the contract
address owner;
//used for forcing the door lock
address raspberryAccount = XXXXXXXXX;
uint constant roomsNumber = 5;
//roomsNumber - sender
mapping (uint => address) reservations;
//address - deposit
mapping (address => uint) deposits;
//address - checkin timestamp
mapping (address => uint) checkins;
uint depositFee = 1 ether;
uint costPerSeconds = 0.0000115 ether;
event doorStatus (bool status, uint roomNr);
function HotelReservation (){
owner = msg.sender;
//init reservations
for (uint i=1; i <= roomsNumber; i++)
{
reservations[i] == 0;
}
}
modifier canReserveRoom(uint roomNr) {
bool canReserve = true;
if(roomNr <= 0 || roomNr > 5)
canReserve = false;
//check if sender has another camera reserved
for (uint i=1; i<= roomsNumber ; i++)
{
if (reservations[i] == msg.sender){
canReserve = false;
}
}
//camera is available
if(reservations[roomNr] != 0)
{
canReserve = false;
}
//money for deposit are enought
if(msg.value < depositFee)
{
canReserve = false;
}
require(canReserve);
_;
}
function unlockDoor(uint roomNr) canReserveRoom(roomNr) public payable returns (bool){
deposits[msg.sender] = depositFee;
reservations[roomNr] = msg.sender;
checkins[msg.sender] = block.timestamp;
doorStatus(true, roomNr);
return true;
}
modifier canLeaveRoom(uint roomNr) {
bool canLeave = true;
//no pending reservation
if (reservations[roomNr] != msg.sender){
canLeave = false;
}
require(canLeave);
_;
}
modifier isTheOwner(){
bool forceRoomLock = true;
if(msg.sender != raspberryAccount)
forceRoomLock = false;
require(forceRoomLock);
_;
}
function forceLockDoor(uint roomNr) isTheOwner public returns (bool){
address tenantAddress = reservations[roomNr];
//retrieve all deposit
owner.transfer(deposits[tenantAddress]);
reservations[roomNr] = 0;
deposits[tenantAddress] = 0;
checkins[tenantAddress] = 0;
doorStatus(false, roomNr);
return true;
}
function lockDoor(uint roomNr) canLeaveRoom(roomNr) public payable returns (bool){
//calculate the cost for the usage of the room
uint checkinTimestamp = checkins[msg.sender];
uint datetimeNow = block.timestamp;
uint usage = datetimeNow - checkinTimestamp;
uint usageInSeconds = uint8(usage % 60);
uint totalCost = usageInSeconds * costPerSeconds;
uint refound = deposits[msg.sender] - totalCost;
//send money back (deposit - usage)
msg.sender.transfer(refound);
//send money back to the hotel owner
owner.transfer(totalCost);
//clean information
reservations[roomNr] = 0;
deposits[msg.sender] = 0;
checkins[msg.sender] = 0;
doorStatus(false, roomNr);
return true;
}
}
Using simple buttons inside an html page, unlockDoor and lockDoor
methods do not open the metamask popup for accepting the transaction.
no errors inside the console.
MetaMask automagically injects itself and sets web3. When you pass in localhost as your provider, you're overriding MM and configuring web3 to talk to TestRPC directly. This is the example code from their site:
window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
window.web3 = new Web3(web3.currentProvider);
} else {
console.log('No web3? You should consider trying MetaMask!')
// fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)
window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}
// Now you can start your app & access web3 freely:
startApp()
})
Also, you're not actually sending any ether when calling unlockDoor. You need to specify the amount in a transactionObject
const transactionObj = {
from: accountAddress,
value: web3.toWei(amountInEther, 'ether'),
};
contract.unlockDoor(1, transactionObj, (error, result) => {
if (error)
console.log(error);
else
console.log(result);
});
Note that I haven't specified any gasLimit or gasPrice either, which you typically would. See the web3js documentation for transaction object options.
Using remix with local RPC: unlock door works, lock door generates
error Error: VM Exception while executing transaction: out of gas. A
lot of articles say to increase gas value but it does not work.
Probably I missed something. I do not understand what. Using
javascript virtual machine all methods work properly.
Out of gas exceptions can be difficult to debug and this one is a bit weird. I THINK you're hitting some sort of bug with TestRPC in estimating gas. The method works fine in the Remix VM and can be forced to work when going through MetaMask connected to TestRPC.
If you execute your contract through MetaMask, your lockDoor method will show as a pending transaction waiting for approval in the MetaMask plugin. If you look carefully, you'll notice that the gas limit field is set pretty low (This limit is determined based on the result of web3.eth.estimateGas). It's actually under the 21000 minimum and MetaMask will prevent you from even approving the transaction. However, if you look at the details in Remix, the gas estimate is about 2x the value initially in MM's gas limit field. If you manually change the gas limit value in MM, the transaction will go through. (Note, I think the gas limit field in the Remix UI under the Run tab is ignored when not using the Remix VM). If you connect directly to TestRPC and execute the method with the gas limit below 21000, you'll get the conveniently confusing "out of gas" exception. Usually, when calling methods through your client, you'd specify your own gas limit (see my comments on the transactionObject above).
Probably the double transfer inside the lock method generates
something strange using RPC (and test net). Are these double
operations correct? Do I have to manage them in another way?
You want to be careful how you transfer money out of a contract. Generally speaking, you'll want to follow the withdrawal pattern. Logic that calculates how much you want to send to an address should be separated from the withdraw action itself. Use lockDoor() to determine how much the owner/renter are owed and store that in the contract state. Then use a separate withdraw function to transfer the funds.
based on point 2 and 3: have generated confusion on how to use the
"payable" instruction.
You only need to mark functions payable that are going to receive ether. Functions that send ether out from the contract don't need to be payable. In addition, you can have a contract receive ether without executing any smart contract logic by adding a payable fallback function.

How to interact with user (for example a confirm dialog) during a long running server action?

I have an MVC5 application. There is a specific action which processes an uploaded large CSV file, and sometimes it needs additional information from the user during this task. For example at row 5, the software needs to show a confirm to the user it he really wants to do something with it, etc. In Winforms environment this was very easy, however I have no idea how I could implement the same on the web.
I would prefer a synchronous way, so that the server thread would be blocked until the confirmation. Otherwise I feel I would have to completely rewrite the logic.
What makes things even more difficoult, is that I would not only need the simple confirmation, but also time to time there can be more complex choices for the user, which can't be implemented synchronously on the client side (only the native simple confirm is synchronous AFAIK).
Any suggestions or hints would be appreciated, a complete short guide even more.
EXAMPLE
In this example the client calls a method which returns the numbers 0, 1, 2, ..., 99, 100. Let's say our users potentially hate the numbers which are dividable by 5. We need to implement a feature which allows the users to exclude these numbers if they whish so. Users don't like to plan for the future, so they wish to choose wether they like such a number or not in real time as the processing happens.
[Controller]
public enum ConfirmResult {
Yes = 0,
No = 1,
YesToAll = 2,
NoToAll = 3
}
...
public JsonResult SomeProcessingAction() {
var result = new List<int>();
for (int i = 0; i <= 100; i++) {
if (i%5==0) {
// sketch implementation for example purposes
if (Confirm(string.Format("The number {0} is dividable by 5. Are you sure you want to include it?", i) == ConfirmResult.No)
continue;
}
result.Add(i);
}
return Json(result);
}
public ConfirmResult Confirm(string message) {
// ... show confirm message on client-side and block until the response comes back... or anything else
}
[Javascript]
// sketch...
$.post('mycontroller/someprocessing', function(result) {
$('#results').text("Your final numbers: " + result.join(', '));
});
I threw together an example and put it up on github for you to take a look at.
MVC5 long running input-required example.
Please note, this is not necessarily the best way to design this. It's my initial approach without a lot of thought. There probably are more flexible or complex, or more recommended patterns.
Basically it just stores the state of a Job in the database (using Entity Framework in the example) whenever it changes.
Persisting to disk or database has definite advantages over some type of long running "synchronous" methods.
It does not lock up resources while waiting for input
It safeguards against data loss in case of crashes or server timeouts
It allows flexibility in case you want to run or resume in a scaled out environment, or on a completely different server (e.g. a non-front-facing VM).
It allows better management of currently running jobs.
For this example, I chose not to use Signalr because it wouldn't add significant value. In the case of long running jobs (say, 5+ minutes), sub-second responses are not going to add to the user experience. I would recommend simply polling from javascript every 1-2 seconds. Much simpler.
Note that some of the code is quite hackish; for example, having the Input fields duplicated on the ResumableJobState table.
The flow might look something like this,
upload file > returns filename // not impl in my example
call StartJob(filename) > returns (Json)Job
Poll GetJobState(jobId) > returns (Json)Job
If (Json)Job.RequiredInputType is populated, show the user an appropriate form to post input back
Call PostInput with the correct type of input from the appropriate form
Job will resume
Here's a dump of the main JobController.
public class JobController : Controller
{
private Context _context;
private JobinatorService _jobinatorService;
public JobController()
{
_context = new Context();
_jobinatorService = new JobinatorService(_context);
}
public ActionResult Index()
{
ViewBag.ActiveJobs = _context.LongRunningJobs.Where(t => t.State != "Completed").ToList();//TODO, filter by logged in User
return View();
}
[HttpPost]
public JsonResult StartJob(string filename)//or maybe you've already uploaded and have a fileId instead
{
var jobState = new ResumableJobState
{
CurrentIteration = 0,
InputFile = filename,
OutputFile = filename + "_output.csv"
};
var job = new LongRunningJob
{
State = "Running",
ResumableJobState = jobState
};
_context.ResumableJobStates.Add(jobState);
_context.LongRunningJobs.Add(job);
var result = _context.SaveChanges();
if (result == 0) throw new Exception("Error saving to database");
_jobinatorService.StartOrResume(job);
return Json(job);
}
[HttpGet]
public JsonResult GetJobState(int jobId)
{
var job = _context.LongRunningJobs.Include("ResumableJobState.RequiredInputType").FirstOrDefault(t => t.Id == jobId);
if (job == null)
throw new HttpException(404, "No job found with that Id");
return Json(job, JsonRequestBehavior.AllowGet);
}
[HttpPost]
public JsonResult PostInput(int jobId, RequiredInputType userInput)
{
if (!ModelState.IsValid)
throw new HttpException(500, "Bad input");
var job = _context.LongRunningJobs.Include("ResumableJobState.RequiredInputType").FirstOrDefault(t => t.Id == jobId);
job.ResumableJobState.BoolInput = userInput.BoolValue;
job.ResumableJobState.IntInput = userInput.IntValue;
job.ResumableJobState.FloatInput = userInput.FloatValue;
job.ResumableJobState.StringInput = userInput.StringValue;
_context.SaveChanges();
if (job == null)
throw new HttpException(404, "No job found with that Id");
if (userInput.InputName == job.ResumableJobState.RequiredInputType.InputName)//Do some checks to see if they provided input matching the requirements
_jobinatorService.StartOrResume(job);
//TODO have the jobinator return the State after it's resumed, otherwise we need another Get to check the state.
return Json(job);
}
/// <summary>
/// Stuff this in it's own service. This way, you could use it in other places; for example starting scheduled jobs from a cron job
/// </summary>
public class JobinatorService//Ideally use Dependency Injection, or something good practicey to get an instance of this
{
private Context _context = new Context();
private string _filePath = "";
public JobinatorService(Context context)
{
_context = context;
_filePath = AppDomain.CurrentDomain.GetData("DataDirectory").ToString() + "/";
}
public void StartOrResume(LongRunningJob job)
{
Task.Run(() =>
{
using (var inputFile = System.IO.File.OpenRead(_filePath + job.ResumableJobState.InputFile))
using (var outputFile = System.IO.File.OpenWrite(_filePath + job.ResumableJobState.OutputFile))
{
inputFile.Position = job.ResumableJobState.CurrentIteration;
for (int i = (int)inputFile.Position; i < inputFile.Length; i++)//casting long to int, what could possibly go wrong?
{
if (job.State == "Input Required" && job.ResumableJobState.RequiredInputType != null)
{//We needed input and received it
//You might want to do a switch..case on the various inputs, and branch into different functions
if (job.ResumableJobState.RequiredInputType.InputName == "6*7")
if (job.ResumableJobState.RequiredInputType.IntValue.Value == 42)
break;//Pass Go, collect 42 dollars;
}
outputFile.WriteByte((byte)inputFile.ReadByte());//Don't try this at home!
job.ResumableJobState.CurrentIteration = i;//or row, or line, or however you delimit processing
job.ResumableJobState.InputFileBufferReadPosition = inputFile.Position;//or something
if (i % 7 == 0)
job.ResumableJobState.RequiredInputType = _context.RequiredInputTypes.First(t => t.InputName == "Row 7 Input");
if (i % 42 == 0)
job.ResumableJobState.RequiredInputType = _context.RequiredInputTypes.First(t => t.InputName == "6*7");
if (job.ResumableJobState.RequiredInputType != null)
job.State = "Input Required";
_context.SaveChanges();
if (job.State != "Running")
return;
}
job.State = "Completed";
_context.SaveChanges();
}
});
return;
}
}
}

Resources